(function () { /** * @license almond 0.3.1 Copyright (c) 2011-2014, The Dojo Foundation All Rights Reserved. * Available via the MIT or new BSD license. * see: http://github.com/jrburke/almond for details */ //Going sloppy to avoid 'use strict' string cost, but strict practices should //be followed. /*jslint sloppy: true */ /*global setTimeout: false */ var requirejs, require, define; (function (undef) { var main, req, makeMap, handlers, defined = {}, waiting = {}, config = {}, defining = {}, hasOwn = Object.prototype.hasOwnProperty, aps = [].slice, jsSuffixRegExp = /\.js$/; function hasProp(obj, prop) { return hasOwn.call(obj, prop); } /** * Given a relative module name, like ./something, normalize it to * a real name that can be mapped to a path. * @param {String} name the relative name * @param {String} baseName a real name that the name arg is relative * to. * @returns {String} normalized name */ function normalize(name, baseName) { var nameParts, nameSegment, mapValue, foundMap, lastIndex, foundI, foundStarMap, starI, i, j, part, baseParts = baseName && baseName.split("/"), map = config.map, starMap = (map && map['*']) || {}; //Adjust any relative paths. if (name && name.charAt(0) === ".") { //If have a base name, try to normalize against it, //otherwise, assume it is a top-level require that will //be relative to baseUrl in the end. if (baseName) { name = name.split('/'); lastIndex = name.length - 1; // Node .js allowance: if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) { name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, ''); } //Lop off the last part of baseParts, so that . matches the //"directory" and not name of the baseName's module. For instance, //baseName of "one/two/three", maps to "one/two/three.js", but we //want the directory, "one/two" for this normalization. name = baseParts.slice(0, baseParts.length - 1).concat(name); //start trimDots for (i = 0; i < name.length; i += 1) { part = name[i]; if (part === ".") { name.splice(i, 1); i -= 1; } else if (part === "..") { if (i === 1 && (name[2] === '..' || name[0] === '..')) { //End of the line. Keep at least one non-dot //path segment at the front so it can be mapped //correctly to disk. Otherwise, there is likely //no path mapping for a path starting with '..'. //This can still fail, but catches the most reasonable //uses of .. break; } else if (i > 0) { name.splice(i - 1, 2); i -= 2; } } } //end trimDots name = name.join("/"); } else if (name.indexOf('./') === 0) { // No baseName, so this is ID is resolved relative // to baseUrl, pull off the leading dot. name = name.substring(2); } } //Apply map config if available. if ((baseParts || starMap) && map) { nameParts = name.split('/'); for (i = nameParts.length; i > 0; i -= 1) { nameSegment = nameParts.slice(0, i).join("/"); if (baseParts) { //Find the longest baseName segment match in the config. //So, do joins on the biggest to smallest lengths of baseParts. for (j = baseParts.length; j > 0; j -= 1) { mapValue = map[baseParts.slice(0, j).join('/')]; //baseName segment has config, find if it has one for //this name. if (mapValue) { mapValue = mapValue[nameSegment]; if (mapValue) { //Match, update name to the new value. foundMap = mapValue; foundI = i; break; } } } } if (foundMap) { break; } //Check for a star map match, but just hold on to it, //if there is a shorter segment match later in a matching //config, then favor over this star map. if (!foundStarMap && starMap && starMap[nameSegment]) { foundStarMap = starMap[nameSegment]; starI = i; } } if (!foundMap && foundStarMap) { foundMap = foundStarMap; foundI = starI; } if (foundMap) { nameParts.splice(0, foundI, foundMap); name = nameParts.join('/'); } } return name; } function makeRequire(relName, forceSync) { return function () { //A version of a require function that passes a moduleName //value for items that may need to //look up paths relative to the moduleName var args = aps.call(arguments, 0); //If first arg is not require('string'), and there is only //one arg, it is the array form without a callback. Insert //a null so that the following concat is correct. if (typeof args[0] !== 'string' && args.length === 1) { args.push(null); } return req.apply(undef, args.concat([relName, forceSync])); }; } function makeNormalize(relName) { return function (name) { return normalize(name, relName); }; } function makeLoad(depName) { return function (value) { defined[depName] = value; }; } function callDep(name) { if (hasProp(waiting, name)) { var args = waiting[name]; delete waiting[name]; defining[name] = true; main.apply(undef, args); } if (!hasProp(defined, name) && !hasProp(defining, name)) { throw new Error('No ' + name); } return defined[name]; } //Turns a plugin!resource to [plugin, resource] //with the plugin being undefined if the name //did not have a plugin prefix. function splitPrefix(name) { var prefix, index = name ? name.indexOf('!') : -1; if (index > -1) { prefix = name.substring(0, index); name = name.substring(index + 1, name.length); } return [prefix, name]; } /** * Makes a name map, normalizing the name, and using a plugin * for normalization if necessary. Grabs a ref to plugin * too, as an optimization. */ makeMap = function (name, relName) { var plugin, parts = splitPrefix(name), prefix = parts[0]; name = parts[1]; if (prefix) { prefix = normalize(prefix, relName); plugin = callDep(prefix); } //Normalize according if (prefix) { if (plugin && plugin.normalize) { name = plugin.normalize(name, makeNormalize(relName)); } else { name = normalize(name, relName); } } else { name = normalize(name, relName); parts = splitPrefix(name); prefix = parts[0]; name = parts[1]; if (prefix) { plugin = callDep(prefix); } } //Using ridiculous property names for space reasons return { f: prefix ? prefix + '!' + name : name, //fullName n: name, pr: prefix, p: plugin }; }; function makeConfig(name) { return function () { return (config && config.config && config.config[name]) || {}; }; } handlers = { require: function (name) { return makeRequire(name); }, exports: function (name) { var e = defined[name]; if (typeof e !== 'undefined') { return e; } else { return (defined[name] = {}); } }, module: function (name) { return { id: name, uri: '', exports: defined[name], config: makeConfig(name) }; } }; main = function (name, deps, callback, relName) { var cjsModule, depName, ret, map, i, args = [], callbackType = typeof callback, usingExports; //Use name if no relName relName = relName || name; //Call the callback to define the module, if necessary. if (callbackType === 'undefined' || callbackType === 'function') { //Pull out the defined dependencies and pass the ordered //values to the callback. //Default to [require, exports, module] if no deps deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps; for (i = 0; i < deps.length; i += 1) { map = makeMap(deps[i], relName); depName = map.f; //Fast path CommonJS standard dependencies. if (depName === "require") { args[i] = handlers.require(name); } else if (depName === "exports") { //CommonJS module spec 1.1 args[i] = handlers.exports(name); usingExports = true; } else if (depName === "module") { //CommonJS module spec 1.1 cjsModule = args[i] = handlers.module(name); } else if (hasProp(defined, depName) || hasProp(waiting, depName) || hasProp(defining, depName)) { args[i] = callDep(depName); } else if (map.p) { map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {}); args[i] = defined[depName]; } else { throw new Error(name + ' missing ' + depName); } } ret = callback ? callback.apply(defined[name], args) : undefined; if (name) { //If setting exports via "module" is in play, //favor that over return value and exports. After that, //favor a non-undefined return value over exports use. if (cjsModule && cjsModule.exports !== undef && cjsModule.exports !== defined[name]) { defined[name] = cjsModule.exports; } else if (ret !== undef || !usingExports) { //Use the return value from the function. defined[name] = ret; } } } else if (name) { //May just be an object definition for the module. Only //worry about defining if have a module name. defined[name] = callback; } }; requirejs = require = req = function (deps, callback, relName, forceSync, alt) { if (typeof deps === "string") { if (handlers[deps]) { //callback in this case is really relName return handlers[deps](callback); } //Just return the module wanted. In this scenario, the //deps arg is the module name, and second arg (if passed) //is just the relName. //Normalize module name, if it contains . or .. return callDep(makeMap(deps, callback).f); } else if (!deps.splice) { //deps is a config object, not an array. config = deps; if (config.deps) { req(config.deps, config.callback); } if (!callback) { return; } if (callback.splice) { //callback is an array, which means it is a dependency list. //Adjust args if there are dependencies deps = callback; callback = relName; relName = null; } else { deps = undef; } } //Support require(['a']) callback = callback || function () {}; //If relName is a function, it is an errback handler, //so remove it. if (typeof relName === 'function') { relName = forceSync; forceSync = alt; } //Simulate async callback; if (forceSync) { main(undef, deps, callback, relName); } else { //Using a non-zero value because of concern for what old browsers //do, and latest browsers "upgrade" to 4 if lower value is used: //http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout: //If want a value immediately, use require('id') instead -- something //that works in almond on the global level, but not guaranteed and //unlikely to work in other AMD implementations. setTimeout(function () { main(undef, deps, callback, relName); }, 4); } return req; }; /** * Just drops the config on the floor, but returns req in case * the config return value is used. */ req.config = function (cfg) { return req(cfg); }; /** * Expose module registry for debugging and tooling */ requirejs._defined = defined; define = function (name, deps, callback) { if (typeof name !== 'string') { throw new Error('See almond README: incorrect module build, no module name'); } //This module may not have dependencies if (!deps.splice) { //deps is not an array, so probably means //an object literal or factory function for //the value. Adjust args. callback = deps; deps = []; } if (!hasProp(defined, name) && !hasProp(waiting, name)) { waiting[name] = [name, deps, callback]; } }; define.amd = { jQuery: true }; }()); define("../lib/almond", function(){}); define( 'models/fieldErrorModel',[], function() { var model = Backbone.Model.extend( { } ); return model; } ); define( 'models/fieldErrorCollection',['models/fieldErrorModel'], function( errorModel ) { var collection = Backbone.Collection.extend( { model: errorModel } ); return collection; } ); define( 'models/fieldModel',['models/fieldErrorCollection'], function( fieldErrorCollection ) { var model = Backbone.Model.extend( { defaults: { placeholder: '', value: '', label_pos: '', classes: 'ninja-forms-field', reRender: false, mirror_field: false, confirm_field: false, clean: true, disabled: '', visible: true, invalid: false }, initialize: function() { var type = this.get('type'); this.set( 'formID', this.collection.options.formModel.get( 'id' ) ); this.listenTo( nfRadio.channel( 'form-' + this.get( 'formID' ) ), 'reset', this.resetModel ); this.bind( 'change', this.changeModel, this ); this.bind( 'change:value', this.changeValue, this ); this.set( 'errors', new fieldErrorCollection() ); if (type === 'listimage') { this.get = this.listimageGet; this.set = this.listimageSet; } /* * Trigger an init event on two channels: * * fields * field-type * * This lets specific field types modify model attributes before anything uses them. */ nfRadio.channel( 'fields' ).trigger( 'init:model', this ); nfRadio.channel( this.get( 'type' ) ).trigger( 'init:model', this ); nfRadio.channel( 'fields-' + this.get( 'type' ) ).trigger( 'init:model', this ); if( 'undefined' != typeof this.get( 'parentType' ) ){ nfRadio.channel( this.get( 'parentType' ) ).trigger( 'init:model', this ); } /* * When we load our form, fire another event for this field. */ this.listenTo( nfRadio.channel( 'form-' + this.get( 'formID' ) ), 'loaded', this.formLoaded ); /* * Before we submit our form, send out a message so that this field can be modified if necessary. */ this.listenTo( nfRadio.channel( 'form-' + this.get( 'formID' ) ), 'before:submit', this.beforeSubmit ); }, listimageGet: function(attr) { if(attr === 'options') { attr = 'image_options'; } return Backbone.Model.prototype.get.call(this, attr); }, listimageSet: function(attributes, options) { if ('options' === attributes) { attributes = 'image_options'; } return Backbone.Model.prototype.set.call(this, attributes, options); }, changeModel: function() { nfRadio.channel( 'field-' + this.get( 'id' ) ).trigger( 'change:model', this ); nfRadio.channel( this.get( 'type' ) ).trigger( 'change:model', this ); nfRadio.channel( 'fields' ).trigger( 'change:model', this ); }, changeValue: function() { nfRadio.channel( 'field-' + this.get( 'id' ) ).trigger( 'change:modelValue', this ); nfRadio.channel( this.get( 'type' ) ).trigger( 'change:modelValue', this ); nfRadio.channel( 'fields' ).trigger( 'change:modelValue', this ); }, addWrapperClass: function( cl ) { this.set( 'addWrapperClass', cl ); }, removeWrapperClass: function( cl ) { this.set( 'removeWrapperClass', cl ); }, setInvalid: function( invalid ) { this.set( 'invalid', invalid ); }, formLoaded: function() { nfRadio.channel( 'fields' ).trigger( 'formLoaded', this ); nfRadio.channel( 'fields-' + this.get( 'type' ) ).trigger( 'formLoaded', this ); }, beforeSubmit: function( formModel ) { nfRadio.channel( this.get( 'type' ) ).trigger( 'before:submit', this ); nfRadio.channel( 'fields' ).trigger( 'before:submit', this ); }, /** * Return the value of this field. * This method exists so that more complex fields can return more than just the field value. * Those advanced fields should create their own method with this name. * * @since 3.5 * @return {string} Value of this field. */ getValue: function() { return this.get( 'value' ); } } ); return model; } ); define( 'models/fieldCollection',['models/fieldModel'], function( fieldModel ) { var collection = Backbone.Collection.extend( { model: fieldModel, comparator: 'order', initialize: function( models, options ) { this.options = options; this.on( 'reset', function( fieldCollection ){ nfRadio.channel( 'fields' ).trigger( 'reset:collection', fieldCollection ); }, this ); }, validateFields: function() { _.each( this.models, function( fieldModel ) { // added here for help with multi-part part validation fieldModel.set( 'clean', false ); nfRadio.channel( 'submit' ).trigger( 'validate:field', fieldModel ); }, this ); }, showFields: function() { this.invoke( 'set', { visible: true } ); this.invoke( function() { this.trigger( 'change:value', this ); }); }, hideFields: function() { this.invoke( 'set', { visible: false } ); this.invoke( function() { this.trigger( 'change:value', this ); }); } } ); return collection; } ); define( 'models/formErrorModel',[], function() { var model = Backbone.Model.extend( { } ); return model; } ); define( 'models/formErrorCollection',['models/formErrorModel'], function( errorModel ) { var collection = Backbone.Collection.extend( { model: errorModel } ); return collection; } ); define( 'models/formModel',[ 'models/fieldCollection', 'models/formErrorCollection' ], function( FieldCollection, ErrorCollection ) { var model = Backbone.Model.extend({ defaults: { beforeForm: '', afterForm: '', beforeFields: '', afterFields: '', wrapper_class: '', element_class: '', hp: '', fieldErrors: {}, extra: {} }, initialize: function() { // Loop over settings and map to attributes _.each( this.get( 'settings' ), function( value, setting ) { this.set( setting, value ); }, this ); this.set( 'loadedFields', this.get( 'fields' ) ); this.set( 'fields', new FieldCollection( this.get( 'fields' ), { formModel: this } ) ); this.set( 'errors', new ErrorCollection() ); /* * Send out a radio message so that anyone who wants to filter our content data can register their filters. */ nfRadio.channel( 'form' ).trigger( 'before:filterData', this ); /* * Set our formContentData to our form setting 'formContentData' */ var formContentData = this.get( 'formContentData' ); /* * The formContentData variable used to be fieldContentsData. * If we don't have a 'formContentData' setting, check to see if we have an old 'fieldContentsData'. * * TODO: This is for backwards compatibility and should be removed eventually. */ if ( ! formContentData ) { formContentData = this.get( 'fieldContentsData' ); } var formContentLoadFilters = nfRadio.channel( 'formContent' ).request( 'get:loadFilters' ); /* * Get our first filter, this will be the one with the highest priority. */ var sortedArray = _.without( formContentLoadFilters, undefined ); var callback = _.first( sortedArray ); formContentData = callback( formContentData, this, this ); this.set( 'formContentData', formContentData ); nfRadio.channel( 'forms' ).trigger( 'init:model', this ); nfRadio.channel( 'form-' + this.get( 'id' ) ).trigger( 'init:model', this ); // Fields nfRadio.channel( 'form-' + this.get( 'id' ) ).reply( 'get:fieldByKey', this.getFieldByKey, this ); // Form Errors nfRadio.channel( 'form-' + this.get( 'id' ) ).reply( 'add:error', this.addError, this ); nfRadio.channel( 'form-' + this.get( 'id' ) ).reply( 'remove:error', this.removeError, this ); // Extra Data nfRadio.channel( 'form-' + this.get( 'id' ) ).reply( 'get:extra', this.getExtra, this ); nfRadio.channel( 'form-' + this.get( 'id' ) ).reply( 'add:extra', this.addExtra, this ); nfRadio.channel( 'form-' + this.get( 'id' ) ).reply( 'remove:extra', this.removeExtra, this ); // Respond to requests to get this model. nfRadio.channel( 'form-' + this.get( 'id' ) ).reply( 'get:form', this.getForm, this ); nfRadio.channel( 'form' ).trigger( 'loaded', this ); nfRadio.channel( 'form' ).trigger( 'after:loaded', this ); nfRadio.channel( 'form-' + this.get( 'id' ) ).trigger( 'loaded', this ); }, /* |-------------------------------------------------------------------------- | Fields |-------------------------------------------------------------------------- */ getFieldByKey: function( key ) { return this.get( 'fields' ).findWhere( { key: key } ); }, /* |-------------------------------------------------------------------------- | Form Errors |-------------------------------------------------------------------------- */ addError: function( id, msg ) { var errors = this.get( 'errors' ); errors.add( { id: id, msg: msg } ); nfRadio.channel( 'form-' + this.get( 'id' ) ).trigger( 'add:error', this, id, msg ); }, removeError: function( id ) { var errors = this.get( 'errors' ); var errorModel = errors.get( id ); errors.remove( errorModel ); nfRadio.channel( 'form-' + this.get( 'id' ) ).trigger( 'remove:error', this, id ); }, /* |-------------------------------------------------------------------------- | Extra Data |-------------------------------------------------------------------------- */ getExtra: function( key ) { var extraData = this.get( 'extra' ); if( 'undefined' == typeof key ) return extraData; return extraData[ key ]; }, addExtra: function( key, value ) { var extraData = this.get( 'extra' ); extraData[ key ] = value; nfRadio.channel( 'form-' + this.get( 'id' ) ).trigger( 'add:extra', this, key, value ); }, removeExtra: function( key ) { var extraData = this.get( 'extra' ); delete extraData[ key ]; nfRadio.channel( 'form-' + this.get( 'id' ) ).trigger( 'remove:extra', this, key ); }, /* |-------------------------------------------------------------------------- | Get this form |-------------------------------------------------------------------------- */ getForm: function() { return this; } } ); return model; } ); define( 'models/formCollection',['models/formModel'], function( formModel ) { var collection = Backbone.Collection.extend( { model: formModel } ); return collection; } ); /* * Handles setting up our form. * * Holds a collection of our fields. * Replies to requests for field data. * Updates field models. */ define('controllers/formData',['models/formModel', 'models/formCollection', 'models/fieldCollection', 'models/formErrorCollection'], function( FormModel, FormCollection, FieldCollection, ErrorCollection ) { var controller = Marionette.Object.extend( { initialize: function() { /* * Setup our field collections. */ var that = this; /* * Initialize our form collection (incase we have multiple forms on the page) */ this.collection = new FormCollection( nfForms ); nfRadio.channel( 'forms' ).trigger( 'loaded', this.collection ); nfRadio.channel( 'app' ).trigger( 'forms:loaded', this.collection ); nfRadio.channel( 'app' ).reply( 'get:form', this.getForm, this ); nfRadio.channel( 'app' ).reply( 'get:forms', this.getForms, this ); nfRadio.channel( 'fields' ).reply( 'get:field', this.getField, this ); }, getForm: function( id ) { return this.collection.get( id ); }, getForms: function() { return this.collection; }, getField: function( id ) { var model = false; _.each( this.collection.models, function( form ) { if ( ! model ) { model = form.get( 'fields' ).get( id ); } } ); if(typeof model == "undefined"){ model = nfRadio.channel( "field-repeater" ).request( 'get:repeaterFieldById', id ); } return model; } }); return controller; } ); define('controllers/fieldError',['models/fieldErrorModel'], function( fieldErrorModel ) { var controller = Marionette.Object.extend( { initialize: function() { nfRadio.channel( 'fields' ).reply( 'add:error', this.addError ); nfRadio.channel( 'fields' ).reply( 'remove:error', this.removeError ); nfRadio.channel( 'fields' ).reply( 'get:error', this.getError ); }, addError: function( targetID, id, msg ) { var model = nfRadio.channel( 'fields' ).request( 'get:field', targetID ); if( 'undefined' == typeof model ) return; var errors = model.get( 'errors' ); errors.add( { 'id': id, 'msg' : msg } ); model.set( 'errors', errors ); model.trigger( 'change:errors', model ); model.set( 'clean', false ); nfRadio.channel( 'fields' ).trigger( 'add:error', model, id, msg ); }, removeError: function( targetID, id ) { var model = nfRadio.channel( 'fields' ).request( 'get:field', targetID ); if( 'undefined' == typeof model ) return; var errors = model.get( 'errors' ); var targetError = errors.get( id ); if ( 'undefined' != typeof targetError ) { errors.remove( targetError ); model.set( 'errors', errors ); model.trigger( 'change:errors', model ); nfRadio.channel( 'fields' ).trigger( 'remove:error', model, id ); } }, getError: function( targetID, id ) { var model = nfRadio.channel( 'fields' ).request( 'get:field', targetID ); var errors = model.get( 'errors' ); var targetError = errors.get( id ); if ( 'undefined' != targetError ) { return targetError; } else { return false; } } }); return controller; } ); /** * Controller responsible for replying to a Radio request stating that a field has been changed. * * This controller sends out a message to the field-specific channel, the field type channel, * and the public fields channel so that the data model can be updated. */ define('controllers/changeField',[], function() { var controller = Marionette.Object.extend( { initialize: function() { /* * Reply to our request for changing a field. */ nfRadio.channel( 'nfAdmin' ).reply( 'change:field', this.changeField ); /* * If we blur our field, set the model attribute of 'clean' to false. * 'clean' tracks whether or not the user has every interacted with this element. * Some validation, like required, uses this to decide whether or not to add an error. */ this.listenTo( nfRadio.channel( 'fields' ), 'blur:field', this.blurField ); }, changeField: function( el, model ) { // Get our current value. var value = nfRadio.channel( model.get( 'type' ) ).request( 'before:updateField', el, model ); value = ( 'undefined' != typeof value ) ? value : nfRadio.channel( model.get( 'parentType' ) ).request( 'before:updateField', el, model ); value = ( 'undefined' != typeof value ) ? value : jQuery( el ).val(); // Set our 'isUpdated' flag to false. model.set( 'isUpdated', false ); // Set our 'clean' flag to false. model.set( 'clean', false ); /* * Send out a message saying that we've changed a field. * The first channel is field id/key specific. * The second channel is the field type, i.e. text, email, radio * The third channel is a generic 'field' channel. * * If the submitted value you wish to store in the data model isn't the same as the value received above, * you can set that model in the actions below and set the 'isUpdated' model attribute to true. * i.e. model.set( 'isUpdated', true ); */ nfRadio.channel( 'field-' + model.get( 'id' ) ).trigger( 'change:field', el, model ); nfRadio.channel( model.get( 'type' ) ).trigger( 'change:field', el, model ); nfRadio.channel( 'fields' ).trigger( 'change:field', el, model ); /* * Send a request out on our nfAdmin channel to update our field model. * If the field model has a 'isUpdated' property of false, nothing will be updated. */ nfRadio.channel( 'nfAdmin' ).request( 'update:field', model, value ); }, blurField: function( el, model ) { // Set our 'clean' flag to false. model.set( 'clean', false ); } }); return controller; } ); define('controllers/changeEmail',[], function() { var radioChannel = nfRadio.channel( 'email' ); // var emailReg = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i; var emailReg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; var errorID = 'invalid-email'; var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( radioChannel, 'change:modelValue', this.onChangeModelValue ); this.listenTo( radioChannel, 'keyup:field', this.emailKeyup ); this.listenTo( radioChannel, 'blur:field', this.onBlurField ); }, onChangeModelValue: function( model ) { var value = model.get( 'value' ); var fieldID = model.get( 'id' ); this.emailChange( value, fieldID ); }, onBlurField: function( el, model ) { var value = jQuery( el ).val(); var fieldID = model.get( 'id' ); this.emailChange( value, fieldID ); }, emailChange: function( value, fieldID ) { if ( 0 < value.length ) { if( emailReg.test( value ) ) { nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID ); } else { var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID ); var formModel = nfRadio.channel( 'app' ).request( 'get:form', fieldModel.get( 'formID' ) ); nfRadio.channel( 'fields' ).request( 'add:error', fieldID, errorID, formModel.get( 'settings' ).changeEmailErrorMsg ); } } else { nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID ); } }, /** * When a user types inside of an email field, track their keypresses and add the appropriate class. * If the value validates as an email, add a class of nf-pass * If the value does not validate as email, add a class of nf-fail * * @since 3.0 * @param {object} el Element that triggered the keyup event. * @param {object} model Model connected to the element that triggered the event * @return {void} */ emailKeyup: function( el, model, keyCode ) { /* * If we pressed the 'tab' key to get to this field, return false. */ if ( 9 == keyCode ) { return false; } /* * Get the current value from our element. */ var value = jQuery( el ).val(); /* * Get our current ID */ var fieldID = model.get( 'id' ); /* * Check our value to see if it is a valid email. */ if ( 0 == value.length ) { nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID ); } else if ( ! emailReg.test( value ) && ! model.get( 'clean' ) ) { var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID ); var formModel = nfRadio.channel( 'app' ).request( 'get:form', fieldModel.get( 'formID' ) ); nfRadio.channel( 'fields' ).request( 'add:error', fieldID, errorID, formModel.get( 'settings' ).changeEmailErrorMsg ); model.removeWrapperClass( 'nf-pass' ); } else if ( emailReg.test( value ) ) { nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID ); /* * Add nf-pass class to the wrapper. */ model.addWrapperClass( 'nf-pass' ); model.set( 'clean', false ); } } }); return controller; } ); define('controllers/changeDate',[], function() { var radioChannel = nfRadio.channel( 'date' ); var errorID = 'invalid-date'; var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( radioChannel, 'change:modelValue', this.onChangeModelValue ); this.listenTo( radioChannel, 'keyup:field', this.dateKeyup ); this.listenTo( radioChannel, 'blur:field', this.onBlurField ); this.listenTo( radioChannel, 'change:extra', this.changeHoursMinutes, this) }, onChangeModelValue: function( model ) { this.dateChange( model ); }, onBlurField: function( el, model ) { this.dateChange( model ); }, dateChange: function( model ) { var fieldID = model.get( 'id' ); var value = model.get( 'value' ); var format = model.get( 'date_format' ); if( 'default' === format) { format = nfi18n.dateFormat; } // If we are dealing with purely a time field, bail early. if ( 'time_only' == model.get( 'date_mode' ) ) { return false; } if ( 0 < value.length ) { // use moment's isValid to check against the fields format setting if( moment( value, format ).isValid() ) { nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID ); } else { var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID ); var formModel = nfRadio.channel( 'app' ).request( 'get:form', fieldModel.get( 'formID' ) ); nfRadio.channel( 'fields' ).request( 'add:error', fieldID, errorID, formModel.get( 'settings' ).changeDateErrorMsg ); } } else { nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID ); } }, /** * When a user types inside of an dat field, track their keypresses * and add the appropriate class. * If the value validates as an date, add a class of nf-pass * If the value does not validate as date, add a class of nf-fail * * @since 3.0 * @param {object} el Element that triggered the keyup event. * @param {object} model Model connected to the element that triggered the event * @return {void} */ dateKeyup: function( el, model, keyCode ) { /* * If we pressed the 'tab' key to get to this field, return false. */ if ( 9 == keyCode ) { return false; } /* * Get the current value from our element. */ var value = jQuery( el ).val(); /* * Get our current ID */ var fieldID = model.get( 'id' ); /* * Get our current date format */ var format = model.get( 'date_format' ); if( 'default' === format) { format = nfi18n.dateFormat; } /* * Check our value to see if it is a valid email. */ if ( 0 == value.length ) { nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID ); } // use moment's isValid to check against the fields format setting else if ( ! moment( value, format ).isValid() && ! model.get( 'clean' ) ) { var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID ); var formModel = nfRadio.channel( 'app' ).request( 'get:form', fieldModel.get( 'formID' ) ); nfRadio.channel( 'fields' ).request( 'add:error', fieldID, errorID, formModel.get( 'settings' ).changeDateErrorMsg ); model.removeWrapperClass( 'nf-pass' ); } // use moment's isValid to check against the fields format setting else if ( moment( value, format ).isValid() ) { nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID ); /* * Add nf-pass class to the wrapper. */ model.addWrapperClass( 'nf-pass' ); model.set( 'clean', false ); } }, changeHoursMinutes: function( e, fieldModel ) { let type = ''; let container = jQuery( e.target ).closest( '.nf-field-element' ); // Set our hour, minute, and ampm let selected_hour = jQuery( container ).find( '.hour' ).val(); let selected_minute = jQuery( container ).find( '.minute' ).val(); let selected_ampm = jQuery( container ).find( '.ampm' ).val(); fieldModel.set( 'selected_hour', selected_hour ); fieldModel.set( 'selected_minute', selected_minute ); fieldModel.set( 'selected_ampm', selected_ampm ); // Trigger a change on our model. fieldModel.trigger( 'change:value', fieldModel ); } }); return controller; } ); define('controllers/fieldCheckbox',[], function() { var controller = Marionette.Object.extend( { initialize: function() { /* * When we init our checkbox model, register our renderClasses() function */ this.listenTo( nfRadio.channel( 'checkbox' ), 'init:model', this.registerRenderClasses ); nfRadio.channel( 'checkbox' ).reply( 'validate:required', this.validateRequired ); nfRadio.channel( 'checkbox' ).reply( 'validate:modelData', this.validateModelData ); nfRadio.channel( 'checkbox' ).reply( 'before:updateField', this.beforeUpdateField, this ); nfRadio.channel( 'checkbox' ).reply( 'get:calcValue', this.getCalcValue, this ); }, beforeUpdateField: function( el, model ) { var checked = jQuery( el ).prop( 'checked' ); if ( checked ) { var value = 1; jQuery( el ).addClass( 'nf-checked' ); jQuery( el ).closest( '.field-wrap' ).find( 'label[for="' + jQuery( el ).prop( 'id' ) + '"]' ).addClass( 'nf-checked-label' ); } else { var value = 0; jQuery( el ).removeClass( 'nf-checked' ); jQuery( el ).closest( '.field-wrap' ).find( 'label[for="' + jQuery( el ).prop( 'id' ) + '"]' ).removeClass( 'nf-checked-label' ); } return value; }, validateRequired: function( el, model ) { return el[0].checked; }, validateModelData: function( model ) { return model.get( 'value' ) != 0; }, getCalcValue: function( fieldModel ) { if ( 1 == fieldModel.get( 'value' ) ) { calcValue = fieldModel.get( 'checked_calc_value' ); } else { calcValue = fieldModel.get( 'unchecked_calc_value' ); } return calcValue; }, registerRenderClasses: function( model ) { if ( 'checked' == model.get( 'default_value' ) ) { model.set( 'value', 1 ); } else { model.set( 'value', 0 ); } model.set( 'customClasses', this.customClasses ); model.set( 'customLabelClasses', this.customLabelClasses ); model.set( 'maybeChecked', this.maybeChecked ); }, customClasses: function( classes ) { if ( 1 == this.value || ( this.clean && 'undefined' != typeof this.default_value && 'checked' == this.default_value ) ) { classes += ' nf-checked'; } else { classes.replace( 'nf-checked', '' ); } return classes; }, customLabelClasses: function( classes ) { if ( 1 == this.value || ( this.clean && 'undefined' != typeof this.default_value && 'checked' == this.default_value ) ) { classes += ' nf-checked-label'; } else { classes.replace( 'nf-checked-label', '' ); } return classes; }, maybeChecked: function() { if ( 1 == this.value || ( this.clean && 'undefined' != typeof this.default_value && 'checked' == this.default_value ) ) { return ' checked'; } else { return ''; } } }); return controller; } ); define('controllers/fieldCheckboxList',[], function() { var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( nfRadio.channel( 'listcheckbox' ), 'init:model', this.register ); this.listenTo( nfRadio.channel( 'terms' ), 'init:model', this.register ); nfRadio.channel( 'listcheckbox' ).reply( 'before:updateField', this.beforeUpdateField, this ); nfRadio.channel( 'terms' ).reply( 'before:updateField', this.beforeUpdateField, this ); nfRadio.channel( 'listcheckbox' ).reply( 'get:calcValue', this.getCalcValue, this ); nfRadio.channel( 'terms' ).reply( 'get:calcValue', this.getCalcValue, this ); }, register: function( model ) { model.set( 'renderOptions', this.renderOptions ); model.set( 'renderOtherText', this.renderOtherText ); model.set( 'selected', [] ); /* * When we init a model, we need to set our 'value' to the selected option's value. * This is the list equivalent of a 'default value'. */ if ( 0 != model.get( 'options' ).length ) { var selected = _.filter( model.get( 'options' ), function( opt ) { return 1 == opt.selected } ); selected = _.map( selected, function( opt ) { return opt.value } ); } /* * This part is re-worked to take into account custom user-meta * values for fields. */ var savedVal = model.get( 'value' ); if( 'undefined' !== typeof savedVal && Array.isArray( savedVal ) ) { model.set( 'value', savedVal ); } else if ( 'undefined' != typeof selected ) { model.set( 'value', selected ); } }, renderOptions: function() { var html = ''; if ( '' == this.value || ( Array.isArray( this.value ) && 0 < this.value.length ) || 0 < this.value.length ) { var valueFound = true; } else { var valueFound = false; } _.each( this.options, function( option, index ) { if( Array.isArray( this.value ) ) { if( Array.isArray( this.value[ 0 ] ) && -1 !== _.indexOf( this.value[ 0 ], option.value ) ) { valueFound = true; } else if( _.indexOf( this.value, option.value ) ) { valueFound = true; } } if ( option.value == this.value ) { valueFound = true; } /* * TODO: This is a bandaid fix for making sure that each option has a "visible" property. * This should be moved to creation so that when an option is added, it has a visible property by default. */ if ( 'undefined' == typeof option.visible ) { option.visible = true; } option.fieldID = this.id; option.classes = this.classes; option.index = index; var selected = false; /* * This part has been re-worked to account for values passed in * via custom user-meta ( a la User Mgmt add-on) */ if( Array.isArray( this.value ) && 0 < this.value.length ) { if ( -1 !== _.indexOf( this.value[ 0 ].split( ',' ), option.value ) || -1 !== _.indexOf( this.value, option.value ) ) { selected = true; } } else if ( ! _.isArray( this.value ) && option.value == this.value ) { selected = true; } else if ( ( 1 == option.selected && this.clean ) && 'undefined' === typeof this.value ) { selected = true; } // else if( ( option.selected && "0" != option.selected ) && this.clean ){ // isSelected = true; // } else { // var testValues = _.map( this.value, function( value ) { // return value.toString(); // } ); // // option.isSelected = ( -1 != testValues.indexOf( option.value.toString() ) ); // } option.selected = selected; option.isSelected = selected; option.required = this.required; var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-field-listcheckbox-option' ); html += template( option ); }, this ); if ( 1 == this.show_other ) { if ( 'nf-other' == this.value ) { valueFound = false; } var data = { fieldID: this.id, classes: this.classes, currentValue: this.value, renderOtherText: this.renderOtherText, valueFound: valueFound }; var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-field-listcheckbox-other' ); html += template( data ); } return html; }, renderOtherText: function() { if ( 'nf-other' == this.currentValue || ! this.valueFound ) { if ( 'nf-other' == this.currentValue ) { this.currentValue = ''; } var data = { fieldID: this.fieldID, classes: this.classes, currentValue: this.currentValue }; var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-field-listcheckbox-other-text' ); return template( data ); } }, getCalcValue: function( fieldModel ) { var calc_value = 0; var options = fieldModel.get( 'options' ); if ( 0 != options.length ) { _.each( fieldModel.get( 'value' ), function( val ) { var tmp_opt = _.find( options, function( opt ) { return opt.value == val } ); calc_value = Number( calc_value ) + Number( tmp_opt.calc ); } ); } return calc_value; }, beforeUpdateField: function( el, model ) { var selected = model.get( 'value' ) || []; if ( typeof selected == 'string' ) selected = [ selected ]; var value = jQuery( el ).val(); var checked = jQuery( el ).prop( 'checked' ); if ( checked ) { selected.push( value ); jQuery( el ).addClass( 'nf-checked' ); jQuery( el ).parent().find( 'label[for="' + jQuery( el ).prop( 'id' ) + '"]' ).addClass( 'nf-checked-label' ); } else { jQuery( el ).removeClass( 'nf-checked' ); jQuery( el ).parent().find( 'label[for="' + jQuery( el ).prop( 'id' ) + '"]' ).removeClass( 'nf-checked-label' ); var i = selected.indexOf( value ); if( -1 != i ){ selected.splice( i, 1 ); } else if ( Array.isArray( selected ) ) { var optionArray = selected[0].split( ',' ); var valueIndex = optionArray.indexOf( value ); if( -1 !== valueIndex) { optionArray.splice( valueIndex, 1 ); } selected = optionArray.join( ',' ); } } // if ( 1 == model.get( 'show_other' ) ) { // model.set( 'reRender', true ); // } return _.clone( selected ); } }); return controller; } ); define('controllers/fieldImageList',[], function() { var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( nfRadio.channel( 'listimage' ), 'init:model', this.register ); nfRadio.channel( 'listimage' ).reply( 'before:updateField', this.beforeUpdateField, this ); nfRadio.channel( 'listimage' ).reply( 'get:calcValue', this.getCalcValue, this ); }, register: function( model ) { model.set( 'renderOptions', this.renderOptions ); model.set( 'renderOtherText', this.renderOtherText ); model.set( 'selected', [] ); /* * When we init a model, we need to set our 'value' to the selected option's value. * This is the list equivalent of a 'default value'. */ if ( 0 != model.get( 'image_options' ).length ) { var selected = _.filter( model.get( 'image_options' ), function( opt ) { return 1 == opt.selected } ); selected = _.map( selected, function( opt ) { return opt.value } ); } /* * This part is re-worked to take into account custom user-meta * values for fields. */ var savedVal = model.get( 'value' ); if( 'undefined' !== typeof savedVal && Array.isArray( savedVal ) ) { model.set( 'value', savedVal ); } else if ( 'undefined' != typeof selected ) { model.set( 'value', selected ); } }, renderOptions: function() { var html = ''; if ( '' == this.value || ( Array.isArray( this.value ) && 0 < this.value.length ) || 0 < this.value.length ) { var valueFound = true; } else { var valueFound = false; } if (this.allow_multi_select === 1) { this.old_classname = 'list-checkbox'; this.image_type = 'checkbox'; } else { this.image_type = 'radio'; } if(this.list_orientation === 'horizontal') { this.flex_direction = 'row'; } else { this.flex_direction = 'column'; } var that = this; var num_columns = parseInt(this.num_columns) || 1; var current_column = 1; var current_row = 1; _.each( this.image_options, function( image, index ) { if (!this.show_option_labels) { image.label = ''; } if( Array.isArray( this.value ) ) { if( Array.isArray( this.value[ 0 ] ) && -1 !== _.indexOf( this.value[ 0 ], image.value ) ) { valueFound = true; } else if( _.indexOf( this.value, image.value ) ) { valueFound = true; } } if ( image.value == this.value ) { valueFound = true; } /* * TODO: This is a bandaid fix for making sure that each option has a "visible" property. * This should be moved to creation so that when an option is added, it has a visible property by default. */ if ( 'undefined' == typeof image.visible ) { image.visible = true; } if(that.list_orientation === 'horizontal' && current_column <= num_columns) { image.styles = "margin:auto;grid-column: " + current_column + "; grid-row = " + current_row; if(current_column === num_columns) { current_column = 1; current_row += 1; } else { current_column += 1; } } image.image_type = that.image_type; image.fieldID = this.id; image.classes = this.classes; image.index = index; var selected = false; /* * This part has been re-worked to account for values passed in * via custom user-meta ( a la User Mgmt add-on) */ if( Array.isArray( this.value ) && 0 < this.value.length ) { if ( -1 !== _.indexOf( this.value[ 0 ].split( ',' ), image.value ) || -1 !== _.indexOf( this.value, image.value ) ) { selected = true; } } else if ( ! _.isArray( this.value ) && image.value == this.value ) { selected = true; } else if ( ( 1 == image.selected && this.clean ) && ('undefined' === typeof this.value || '' === this.value)) { selected = true; } image.selected = selected; image.isSelected = selected; image.required = this.required; var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-field-listimage-option' ); html += template( image ); }, this ); if ( 1 == this.show_other ) { if ( 'nf-other' == this.value ) { valueFound = false; } var data = { fieldID: this.id, classes: this.classes, value: this.value, currentValue: this.value, renderOtherText: this.renderOtherText, valueFound: valueFound }; var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-field-listimage-other' ); html += template( data ); } return html; }, renderOtherText: function() { if ( 'nf-other' == this.currentValue || ! this.valueFound ) { if ( 'nf-other' == this.currentValue ) { this.currentValue = ''; } var data = { fieldID: this.fieldID, classes: this.classes, currentValue: this.currentValue }; var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-field-listimage-other-text' ); return template( data ); } }, getCalcValue: function( fieldModel ) { var calc_value = 0; var options = fieldModel.get( 'options' ); if ( 0 != options.length ) { /* * Check to see if this is a multi-select list. */ if ( 1 == parseInt( fieldModel.get( 'allow_multi_select' ) ) ) { /* * We're using a multi-select, so we need to check out any selected options and add them together. */ _.each( fieldModel.get( 'value' ), function( val ) { var tmp_opt = _.find( options, function( opt ) { return opt.value == val } ); calc_value += Number( tmp_opt.calc ); } ); } else { /* * We are using a single select, so our selected option is in the 'value' attribute. */ var selected = _.find( options, function( opt ) { return fieldModel.get( 'value' ) == opt.value } ); /* * If we have a selcted value, use it. */ if ( 'undefined' !== typeof selected ) { calc_value = selected.calc; } } } return calc_value; }, beforeUpdateField: function( el, model ) { if(model.get('allow_multi_select') !== 1) { var selected = jQuery( el ).val(); var options = model.get('image_options'); _.each(options, function(option, index) { if(option.value === selected) { option.isSelected = true; option.selected = true; } else { option.isSelected = false; option.selected = false; } if(!option.isSelected) { option.selected = false; jQuery("#nf-field-" + option.fieldID + "-" + index).removeClass('nf-checked'); jQuery("#nf-label-field-" + option.fieldID + "-" + index).removeClass('nf-checked-label'); } else { jQuery("#nf-field-" + option.fieldID + "-" + index).addClass('nf-checked'); jQuery("#nf-label-field-" + option.fieldID + "-" + index).addClass('nf-checked-label'); } }); } else { var selected = model.get( 'value' ) || []; if ( typeof selected == 'string' ) selected = [ selected ]; var value = jQuery( el ).val(); var checked = jQuery( el ).prop( 'checked' ); if ( checked ) { selected.push( value ); jQuery( el ).addClass( 'nf-checked' ); jQuery( el ).parent().find( 'label[for="' + jQuery( el ).prop( 'id' ) + '"]' ).addClass( 'nf-checked-label' ); } else { jQuery( el ).removeClass( 'nf-checked' ); jQuery( el ).parent().find( 'label[for="' + jQuery( el ).prop( 'id' ) + '"]' ).removeClass( 'nf-checked-label' ); var i = selected.indexOf( value ); if( -1 != i ){ selected.splice( i, 1 ); } else if ( Array.isArray( selected ) ) { var optionArray = selected[0].split( ',' ); var valueIndex = optionArray.indexOf( value ); if( -1 !== valueIndex) { optionArray.splice( valueIndex, 1 ); } selected = optionArray.join( ',' ); } } } return _.clone( selected ); } }); return controller; } ); define('controllers/fieldRadio',[], function() { var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( nfRadio.channel( 'listradio' ), 'change:modelValue', this.changeModelValue ); this.listenTo( nfRadio.channel( 'listradio' ), 'init:model', this.register ); nfRadio.channel( 'listradio' ).reply( 'get:calcValue', this.getCalcValue, this ); this.listenTo( nfRadio.channel( 'listradio' ), 'change:field', this.updateCheckedClass, this ); }, register: function( model ) { model.set( 'renderOptions', this.renderOptions ); model.set( 'renderOtherText', this.renderOtherText ); /* * When we init a model, we need to set our 'value' to the selected option's value. * This is the list equivalent of a 'default value'. */ if ( 0 != model.get( 'options' ).length ) { /* * Check to see if we have a selected value. */ var selected = _.find( model.get( 'options' ), function( opt ) { return 1 == opt.selected } ); if ( 'undefined' != typeof selected ) { model.set( 'value', selected.value ); } } }, changeModelValue: function( model ) { if ( 1 == model.get( 'show_other' ) ) { // model.set( 'reRender', true ); model.trigger( 'reRender'); } }, renderOptions: function() { var html = ''; if ( '' == this.value ) { var valueFound = true; } else { var valueFound = false; } _.each( this.options, function( option, index ) { if ( option.value == this.value ) { valueFound = true; } /* * TODO: This is a bandaid fix for making sure that each option has a "visible" property. * This should be moved to creation so that when an option is added, it has a visible property by default. */ if ( 'undefined' == typeof option.visible ) { option.visible = true; } option.selected = false; option.fieldID = this.id; option.classes = this.classes; option.currentValue = this.value; option.index = index; option.required = this.required; /* * If we haven't edited this field yet, use the default checked */ if ( this.clean && 1 == this.selected ) { option.selected = true; } else if ( this.value == option.value ) { option.selected = true; } else { option.selected = false; } var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-field-listradio-option' ); html += template( option ); }, this ); if ( 1 == this.show_other ) { if ( 'nf-other' == this.value ) { valueFound = false; } var data = { fieldID: this.id, classes: this.classes, currentValue: this.value, renderOtherText: this.renderOtherText, valueFound: valueFound }; var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-field-listradio-other' ); html += template( data ); } return html; }, renderOtherText: function() { if ( 'nf-other' == this.currentValue || ! this.valueFound ) { if ( 'nf-other' == this.currentValue ) { this.currentValue = ''; } var data = { fieldID: this.fieldID, classes: this.classes, currentValue: this.currentValue }; var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-field-listradio-other-text' ); return template( data ); } }, getCalcValue: function( fieldModel ) { /* * Default to 0, in case we have no selection. */ var calc_value = 0; if ( 0 != fieldModel.get( 'options' ).length ) { /* * Check to see if we have a selected value. */ var selected = _.find( fieldModel.get( 'options' ), function( opt ) { return fieldModel.get( 'value' ) == opt.value } ); if ( 'undefined' !== typeof selected ) { calc_value = selected.calc; } } return calc_value; }, updateCheckedClass: function( el, model ) { jQuery( '[name="' + jQuery( el ).attr( 'name' ) + '"]' ).removeClass( 'nf-checked' ); jQuery( el ).closest( 'ul' ).find( 'label' ).removeClass( 'nf-checked-label' ); jQuery( el ).addClass( 'nf-checked' ); jQuery( el ).closest( 'li' ).find( 'label[for="' + jQuery( el ).prop( 'id' ) + '"]' ).addClass( 'nf-checked-label' ); } }); return controller; } ); define('controllers/fieldNumber',[], function() { var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( nfRadio.channel( 'number' ), 'init:model', this.maybeMinDefault ); this.listenTo( nfRadio.channel( 'number' ), 'keyup:field', this.validateMinMax ); }, maybeMinDefault: function( model ) { if( '' == model.get( 'value' ) && '' == model.get( 'placeholder' ) ){ var min = model.get( 'num_min' ); model.set( 'placeholder', min ); } }, validateMinMax: function( el, model ) { var $el = jQuery( el ); var value = parseFloat( $el.val() ); var min = $el.attr( 'min' ); var max = $el.attr( 'max' ); var step = parseFloat( $el.attr( 'step' ) ); if( min && value < min ){ var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', model.get( 'id' ) ); var formModel = nfRadio.channel( 'app' ).request( 'get:form', fieldModel.get( 'formID' ) ); nfRadio.channel( 'fields' ).request( 'add:error', model.get( 'id' ), 'number-min', formModel.get( 'settings' ).fieldNumberNumMinError ); } else { nfRadio.channel( 'fields' ).request( 'remove:error', model.get( 'id' ), 'number-min' ); } if ( max && value > max ){ var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', model.get( 'id' ) ); var formModel = nfRadio.channel( 'app' ).request( 'get:form', fieldModel.get( 'formID' ) ); nfRadio.channel( 'fields' ).request( 'add:error', model.get( 'id' ), 'number-max', formModel.get( 'settings' ).fieldNumberNumMaxError ); } else { nfRadio.channel( 'fields' ).request( 'remove:error', model.get( 'id' ), 'number-max' ); } var testValue = Math.round( parseFloat( value ) * 1000000000 ); var testStep = Math.round( parseFloat( step ) * 1000000000 ); if( value && 0 !== testValue % testStep ){ var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', model.get( 'id' ) ); var formModel = nfRadio.channel( 'app' ).request( 'get:form', fieldModel.get( 'formID' ) ); nfRadio.channel( 'fields' ).request( 'add:error', model.get( 'id' ), 'number-step', formModel.get( 'settings' ).fieldNumberIncrementBy + step ); } else { nfRadio.channel( 'fields' ).request( 'remove:error', model.get( 'id' ), 'number-step' ); } } }); return controller; } ); define( 'controllers/mirrorField',[], function() { var radioChannel = nfRadio.channel( 'fields' ); var controller = Marionette.Object.extend( { listeningModel: '', initialize: function() { this.listenTo( radioChannel, 'init:model', this.registerMirror ); }, registerMirror: function( model ) { if ( model.get( 'mirror_field' ) ) { this.listeningModel = model; var targetID = model.get( 'mirror_field' ); this.listenTo( nfRadio.channel( 'field-' + targetID ), 'change:modelValue', this.changeValue ); } }, changeValue: function( targetModel ) { this.listeningModel.set( 'value', targetModel.get( 'value' ) ); // this.listeningModel.set( 'reRender', true ); this.listeningModel.trigger( 'reRender' ); } }); return controller; } ); define( 'controllers/confirmField',[], function() { var radioChannel = nfRadio.channel( 'fields' ); var errorID = 'confirm-mismatch'; var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( radioChannel, 'init:model', this.registerConfirm ); this.listenTo( radioChannel, 'keyup:field', this.confirmKeyup ); }, registerConfirm: function( confirmModel ) { if ( ! confirmModel.get( 'confirm_field' ) ) return; this.listenTo( nfRadio.channel( 'form' ), 'loaded', function( formModal ){ this.registerConfirmListeners( confirmModel ); }); }, registerConfirmListeners: function( confirmModel ) { var targetModel = nfRadio.channel( 'form-' + confirmModel.get( 'formID' ) ).request( 'get:fieldByKey', confirmModel.get( 'confirm_field' ) ); //TODO: Add better handling for password confirm fields on the front end. if( 'undefined' == typeof targetModel ) return; targetModel.set( 'confirm_with', confirmModel.get( 'id' ) ); this.listenTo( nfRadio.channel( 'field-' + targetModel.get( 'id' ) ), 'change:modelValue', this.changeValue ); this.listenTo( nfRadio.channel( 'field-' + confirmModel.get( 'id' ) ), 'change:modelValue', this.changeValue ); }, changeValue: function( model ) { if ( 'undefined' == typeof model.get( 'confirm_with' ) ) { var confirmModel = model; var targetModel = nfRadio.channel( 'form-' + model.get( 'formID' ) ).request( 'get:fieldByKey', confirmModel.get( 'confirm_field' ) ); } else { var targetModel = model; var confirmModel = radioChannel.request( 'get:field', targetModel.get( 'confirm_with' ) ); } var targetID = targetModel.get( 'id' ); var confirmID = confirmModel.get( 'id' ); if ( '' == confirmModel.get( 'value' ) || confirmModel.get( 'value' ) == targetModel.get( 'value' ) ) { nfRadio.channel( 'fields' ).request( 'remove:error', confirmID, errorID ); } else { var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', confirmID ); var formModel = nfRadio.channel( 'app' ).request( 'get:form', fieldModel.get( 'formID' ) ); nfRadio.channel( 'fields' ).request( 'add:error', confirmID, errorID, formModel.get( 'settings' ).confirmFieldErrorMsg ); } }, confirmKeyup: function( el, model, keyCode ) { var currentValue = jQuery( el ).val(); if ( model.get( 'confirm_field' ) ) { var confirmModel = model; var confirmID = model.get( 'id' ); var targetModel = nfRadio.channel( 'form-' + model.get( 'formID' ) ).request( 'get:fieldByKey', confirmModel.get( 'confirm_field' ) ); var compareValue = targetModel.get( 'value' ); var confirmValue = currentValue; } else if ( model.get( 'confirm_with' ) ) { var confirmModel = nfRadio.channel( 'fields' ).request( 'get:field', model.get( 'confirm_with' ) ); var confirmID = confirmModel.get( 'id' ); var confirmValue = confirmModel.get( 'value' ); var compareValue = confirmValue; } if ( 'undefined' !== typeof confirmModel ) { if ( '' == confirmValue ) { nfRadio.channel( 'fields' ).request( 'remove:error', confirmID, errorID ); } else if ( currentValue == compareValue ) { nfRadio.channel( 'fields' ).request( 'remove:error', confirmID, errorID ); } else { var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', confirmID ); var formModel = nfRadio.channel( 'app' ).request( 'get:form', fieldModel.get( 'formID' ) ); nfRadio.channel( 'fields' ).request( 'add:error', confirmID, errorID, formModel.get( 'settings' ).confirmFieldErrorMsg ); } } } }); return controller; } ); define('controllers/updateFieldModel',[], function() { var controller = Marionette.Object.extend( { initialize: function() { nfRadio.channel( 'nfAdmin' ).reply( 'update:field', this.updateField ); }, updateField: function( model, value ) { if ( ! model.get( 'isUpdated' ) ) { model.set( 'value', value ); model.set( 'isUpdated', true ); /* * If we're working with an array, it won't trigger a change event on the value attribute. * Instead, we have to manually trigger a change event. */ if ( _.isArray( value ) ) { model.trigger( 'change:value', model ); } } } }); return controller; } ); define('controllers/submitButton',['controllers/submitButton'], function( submitButton ) { var controller = Marionette.Object.extend( { bound: {}, initialize: function() { this.listenTo( nfRadio.channel( 'submit' ), 'init:model', this.registerHandlers ); }, registerHandlers: function( fieldModel ) { if ( 'undefined' != typeof this.bound[ fieldModel.get( 'id' ) ] ) { return false; } this.listenTo( nfRadio.channel( 'field-' + fieldModel.get( 'id' ) ), 'click:field', this.click, this ); /* * Register an interest in the 'before:submit' event of our form. */ fieldModel.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'before:submit', this.beforeSubmit, fieldModel ); fieldModel.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'submit:failed', this.resetLabel, fieldModel ); fieldModel.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'submit:response', this.resetLabel, fieldModel ); fieldModel.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'enable:submit', this.maybeEnable, fieldModel ); fieldModel.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'disable:submit', this.maybeDisable, fieldModel ); fieldModel.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'processingLabel', this.processingLabel, fieldModel ); fieldModel.listenTo( nfRadio.channel( 'fields' ), 'add:error', this.maybeDisable, fieldModel ); fieldModel.listenTo( nfRadio.channel( 'fields' ), 'remove:error', this.maybeEnable, fieldModel ); this.bound[ fieldModel.get( 'id') ] = true; }, click: function( e, fieldModel ) { var formModel = nfRadio.channel( 'app' ).request( 'get:form', fieldModel.get( 'formID' ) ); nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ).request( 'submit', formModel ); }, beforeSubmit: function() { this.set( 'disabled', true ); nfRadio.channel( 'form-' + this.get( 'formID' ) ).trigger( 'processingLabel', this ); }, maybeDisable: function( fieldModel ) { if( 'undefined' != typeof fieldModel && fieldModel.get( 'formID' ) != this.get( 'formID' ) ) return; this.set( 'disabled', true ); this.trigger( 'reRender' ); }, maybeEnable: function( fieldModel ) { /* * If the field reporting the error is not on the same form as the submit button, return false; */ if ( 'undefined' != typeof fieldModel && fieldModel.get( 'formID' ) != this.get( 'formID' ) ) { return false; } var formModel = nfRadio.channel( 'app' ).request( 'get:form', this.get( 'formID' ) ); if ( 0 == _.size( formModel.get( 'fieldErrors' ) ) ) { this.set( 'disabled', false ); this.trigger( 'reRender' ); } }, processingLabel: function() { if ( this.get( 'label' ) == this.get( 'processing_label' ) ) return false; this.set( 'oldLabel', this.get( 'label' ) ); this.set( 'label', this.get( 'processing_label' ) ); this.trigger( 'reRender' ); }, resetLabel: function( response ) { if ( 'undefined' != typeof response.errors && 'undefined' != typeof response.errors.nonce && _.size( response.errors.nonce ) > 0 ) { if( 'undefined' != typeof response.errors.nonce.new_nonce && 'undefined' != typeof response.errors.nonce.nonce_ts ) { // Do not reset label for nonce errors, which will re-submit the form. return; } } if ( 'undefined' != typeof this.get( 'oldLabel' ) ) { this.set( 'label', this.get( 'oldLabel' ) ); } this.set( 'disabled', false ); this.trigger( 'reRender' ); } }); return controller; } ); define('controllers/submitDebug',[], function() { var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( nfRadio.channel( 'forms' ), 'submit:response', this.submitDebug ); }, submitDebug: function( response, textStatus, jqXHR, formID ) { if( 'undefined' == typeof response.debug ) return; /* Form Debug Messages */ if( 'undefined' != typeof response.debug.form ) { var debugMessages = document.createElement( 'span' ); _.each(response.debug.form, function (message, index) { var messageText = document.createTextNode( message ); debugMessages.appendChild( messageText ); debugMessages.appendChild( document.createElement( 'br' ) ); }); jQuery('.nf-debug-msg').html( debugMessages ); } /* Console Debug Messages */ if( 'undefined' != typeof response.debug.console ) { var style = ''; console.log( '%c%s', style, 'NINJA SUPPORT' ); _.each(response.debug.console, function (message, index) { console.log( message ); }); console.log( '%c%s', style, 'END NINJA SUPPORT' ); } } }); return controller; } ); define('controllers/getFormErrors',[], function() { var radioChannel = nfRadio.channel( 'fields' ); var controller = Marionette.Object.extend( { initialize: function( model ) { nfRadio.channel( 'form' ).reply( 'get:errors', this.getFormErrors ); }, getFormErrors: function( formID ) { var formModel = nfRadio.channel( 'app' ).request( 'get:form', formID ); var errors = false; if ( formModel ) { /* * Check to see if we have any errors on our form model. */ if ( 0 !== formModel.get( 'errors' ).length ) { _.each( formModel.get( 'errors' ).models, function( error ) { errors = errors || {}; errors[ error.get( 'id' ) ] = error.get( 'msg' ); } ); } _.each( formModel.get( 'fields' ).models, function( field ) { if ( field.get( 'type' ) != 'submit' && field.get( 'errors' ).length > 0 ) { errors = errors || {}; errors[ field.get( 'id' ) ] = field.get( 'errors' ); } } ); } return errors; }, }); return controller; } ); define('controllers/validateRequired',[], function() { var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( nfRadio.channel( 'fields' ), 'blur:field', this.validateRequired ); this.listenTo( nfRadio.channel( 'fields' ), 'change:field', this.validateRequired ); this.listenTo( nfRadio.channel( 'fields' ), 'keyup:field', this.validateKeyup ); this.listenTo( nfRadio.channel( 'fields' ), 'change:modelValue', this.validateModelData ); this.listenTo( nfRadio.channel( 'submit' ), 'validate:field', this.validateModelData ); }, validateKeyup: function( el, model, keyCode ) { if ( 1 != model.get( 'required' ) ) { return false; } if ( ! model.get( 'clean' ) ) { this.validateRequired( el, model ); } }, validateRequired: function( el, model ) { if ( 1 != model.get( 'required' ) || ! model.get( 'visible' ) ) { return false; } var currentValue = jQuery( el ).val(); var customReqValidation = nfRadio.channel( model.get( 'type' ) ).request( 'validate:required', el, model ); var defaultReqValidation = true; var maskPlaceholder = model.get( 'mask' ); if ( maskPlaceholder ) { maskPlaceholder = maskPlaceholder.replace( /9/g, '_' ); maskPlaceholder = maskPlaceholder.replace( /a/g, '_' ); maskPlaceholder = maskPlaceholder.replace( /\*/g, '_' ); } // If the field has a mask... // AND that mask is equal to the current value... if ( maskPlaceholder && currentValue === maskPlaceholder ) { // If we have a pre-existing error... if ( 0 < model.get( 'errors' ).length ) { // Persist that error. defaultReqValidation = false; } } // If our value is an empty string... if ( ! jQuery.trim( currentValue ) ) { // Throw an error. defaultReqValidation = false; } if ( 'undefined' !== typeof customReqValidation ) { var valid = customReqValidation; } else { var valid = defaultReqValidation; } this.maybeError( valid, model ); }, validateModelData: function( model ) { if ( 1 != model.get( 'required' ) || ! model.get( 'visible' ) || model.get( 'clean' ) ) { return false; } /* * If we already have a required error on this model, return false */ if ( model.get( 'errors' ).get( 'required-error' ) ) { return false; } currentValue = model.get( 'value' ); var defaultReqValidation = true; if ( ! jQuery.trim( currentValue ) ) { defaultReqValidation = false; } var customReqValidation = nfRadio.channel( model.get( 'type' ) ).request( 'validate:modelData', model ); if ( 'undefined' !== typeof customReqValidation ) { var valid = customReqValidation; } else { var valid = defaultReqValidation; } this.maybeError( valid, model ); }, maybeError: function( valid, model ) { if ( ! valid ) { var formModel = nfRadio.channel( 'form-' + model.get( 'formID' ) ).request( 'get:form' ); if( 'undefined' != typeof formModel ) { nfRadio.channel('fields').request('add:error', model.get('id'), 'required-error', formModel.get('settings').validateRequiredField); } } else { nfRadio.channel( 'fields' ).request( 'remove:error', model.get( 'id' ), 'required-error' ); } } }); return controller; } ); define('controllers/submitError',[], function() { var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( nfRadio.channel( 'forms' ), 'submit:response', this.submitErrors ); }, submitErrors: function( response, textStatus, jqXHR, formID ) { // Check for nonce error. if ( _.size( response.errors.nonce ) > 0 ) { if( 'undefined' != typeof response.errors.nonce.new_nonce && 'undefined' != typeof response.errors.nonce.nonce_ts ) { // Update nonce from response. nfFrontEnd.ajaxNonce = response.errors.nonce.new_nonce; nfFrontEnd.nonce_ts = response.errors.nonce.nonce_ts; // Re-submit form. var formModel = nfRadio.channel( 'app' ).request( 'get:form', formID ); nfRadio.channel( 'form-' + formID ).request( 'submit', formModel ); } } if ( _.size( response.errors.fields ) > 0 ) { _.each( response.errors.fields, function( data, fieldID ) { if ( typeof( data ) === 'object' ) { nfRadio.channel( 'fields' ).request( 'add:error', fieldID, data.slug, data.message ); } else { nfRadio.channel( 'fields' ).request( 'add:error', fieldID, 'required-error', data ); } } ); } if ( _.size( response.errors.form ) > 0 ) { _.each( response.errors.form, function( msg, errorID ) { nfRadio.channel( 'form-' + formID ).request( 'remove:error', errorID ); nfRadio.channel( 'form-' + formID ).request( 'add:error', errorID, msg ); } ); } if ( 'undefined' != typeof response.errors.last ) { if( 'undefined' != typeof response.errors.last.message ) { var style = 'background: rgba( 255, 207, 115, .5 ); color: #FFA700; display: block;'; console.log( '%c NINJA FORMS SUPPORT: SERVER ERROR', style ); console.log( response.errors.last.message ); console.log( '%c END SERVER ERROR MESSAGE', style ); } } /** * TODO: This needs to be re-worked for backbone. It's not dynamic enough. */ /* * Re-show any hidden fields during a form submission re-start. */ jQuery( '#nf-form-' + formID + '-cont .nf-field-container' ).show(); } }); return controller; } ); define('controllers/actionRedirect',[], function() { var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( nfRadio.channel( 'forms' ), 'submit:response', this.actionRedirect ); }, actionRedirect: function( response ) { if ( 'undefined' != typeof response.data.halt && 'undefined' != typeof response.data.halt.redirect && '' != response.data.halt.redirect ) { window.location = response.data.halt.redirect; } if ( _.size( response.errors ) == 0 && 'undefined' != typeof response.data.actions ) { if ( 'undefined' != typeof response.data.actions.redirect && '' != response.data.actions.redirect ) { window.location = response.data.actions.redirect; } } } }); return controller; } ); define('controllers/actionSuccess',[], function() { var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( nfRadio.channel( 'forms' ), 'submit:response', this.actionSubmit ); }, actionSubmit: function( response ) { if ( _.size( response.errors ) == 0 && 'undefined' != typeof response.data.actions ) { if ( 'undefined' != typeof response.data.actions.success_message && '' != response.data.actions.success_message ) { var form_id = response.data.form_id; var success_message = jQuery( '#nf-form-' + form_id + '-cont .nf-response-msg' ); success_message.html( response.data.actions.success_message ).show(); //Let's check if the success message is already fully visible in the viewport without scrolling var top_of_success_message = success_message.offset().top; var bottom_of_success_message = success_message.offset().top + success_message.outerHeight(); var bottom_of_screen = jQuery(window).scrollTop() + jQuery(window).height(); var top_of_screen = jQuery(window).scrollTop(); var the_element_is_visible = ((bottom_of_screen > bottom_of_success_message) && (top_of_screen < top_of_success_message)); if(!the_element_is_visible){ //The element isn't visible, so let's scroll to the success message as in the previous release, but with a short animation jQuery('html, body').animate({ scrollTop: ( success_message.offset().top - 50 ) }, 300 ); } } } } }); return controller; } ); define('controllers/fieldSelect',[], function() { var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( nfRadio.channel( 'fields' ), 'init:model', function( model ){ if( 'list' == model.get( 'parentType' ) ) this.register( model ); }, this ); nfRadio.channel( 'listselect' ).reply( 'get:calcValue', this.getCalcValue, this ); nfRadio.channel( 'listmultiselect' ).reply( 'get:calcValue', this.getCalcValue, this ); }, register: function( model ) { model.set( 'renderOptions', this.renderOptions ); model.set( 'renderOtherAttributes', this.renderOtherAttributes ); /* * When we init a model, we need to set our 'value' to the selected option's value. * This is the list equivalent of a 'default value'. */ if ( 0 != model.get( 'options' ).length ) { //Check to see if there is a value set for the field var savedVal = model.get( 'value' ); /* * Check to see if this is a multi-select list. */ if ( 'listmultiselect' == model.get( 'type' ) ) { /* * We're using a multi-select, so we need to check out any selected options and add them together. */ var selected = _.filter( model.get( 'options' ), function( opt ) { return 1 == opt.selected } ); selected = _.map( selected, function( opt ) { return opt.value } ); var value = selected; } else if ( 'listradio' !== model.get( 'type' ) ) { /* * Check to see if we have a selected value. */ var selected = _.find( model.get( 'options' ), function( opt ) { return 1 == opt.selected } ); /* * We don't have a selected value, so use our first option. */ if ( 'undefined' == typeof selected ) { selected = _.first( model.get( 'options' ) ); } if ( 'undefined' != typeof selected && 'undefined' != typeof selected.value ) { var value = selected.value; } else if ( 'undefined' != typeof selected ) { var value = selected.label; } } /* * This part is re-worked to take into account custom user-meta * values for fields. */ if( 'undefined' !== typeof savedVal && '' !== savedVal && Array.isArray( savedVal ) ) { model.set( 'value', savedVal ); } else if ( 'undefined' != typeof selected ) { model.set( 'value', value ); } } }, renderOptions: function() { var html = ''; _.each( this.options, function( option ) { /* * This part has been re-worked to account for values passed in * via custom user-meta ( a la User Mgmt add-on) */ if ( _.isArray( this.value ) ) { // If we have a multiselect list... // AND it has selected values... if( 'listmultiselect' === this.type && 0 < this.value.length && -1 != _.indexOf( this.value[ 0 ].split( ',' ), option.value ) ) { var selected = true; } else if( -1 != _.indexOf( this.value, option.value ) ) { var selected = true; } } else if ( ! _.isArray( this.value ) && option.value == this.value ) { var selected = true; } else if ( ( 1 == option.selected && this.clean ) && 'undefined' === typeof this.value ) { var selected = true; } else { var selected = false; } /* * TODO: This is a bandaid fix for making sure that each option has a "visible" property. * This should be moved to creation so that when an option is added, it has a visible property by default. */ if ( 'undefined' == typeof option.visible ) { option.visible = true; } option.selected = selected; option.fieldID = this.id; option.classes = this.classes; option.currentValue = this.value; var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-field-listselect-option' ); html += template( option ); }, this ); return html; }, renderOtherAttributes: function() { var otherAttributes = ''; if( 'listmultiselect' == this.type ){ otherAttributes = otherAttributes + ' multiple'; var multiSize = this.multi_size || 5; otherAttributes = otherAttributes + ' size="' + multiSize + '"'; } return otherAttributes; }, getCalcValue: function( fieldModel ) { var calc_value = 0; var options = fieldModel.get( 'options' ); if ( 0 != options.length ) { /* * Check to see if this is a multi-select list. */ if ( 'listmultiselect' == fieldModel.get( 'type' ) ) { /* * We're using a multi-select, so we need to check out any selected options and add them together. */ _.each( fieldModel.get( 'value' ), function( val ) { var tmp_opt = _.find( options, function( opt ) { return opt.value == val } ); calc_value += Number( tmp_opt.calc ); } ); } else { /* * We are using a single select, so our selected option is in the 'value' attribute. */ var selected = _.find( options, function( opt ) { return fieldModel.get( 'value' ) == opt.value } ); /* * We don't have a selected value, so use our first option. */ if ( 'undefined' == typeof selected ) { selected = fieldModel.get( 'options' )[0]; } calc_value = selected.calc; } } return calc_value; } }); return controller; } ); define('controllers/coreSubmitResponse',[], function() { var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( nfRadio.channel( 'forms' ), 'submit:response', this.actionSubmit ); }, actionSubmit: function( response ) { var formModel = nfRadio.channel( 'app' ).request( 'get:form', response.data.form_id ); /* * If we have errors, don't hide or clear. */ if ( 0 != _.size( response.errors ) ) { return false; } if ( 1 == response.data.settings.clear_complete ) { // nfRadio.channel( 'form-' + response.data.form_id ).trigger( 'reset' ); formModel.get( 'fields' ).reset( formModel.get( 'loadedFields' ) ); if ( 1 != response.data.settings.hide_complete ) { nfRadio.channel( 'captcha' ).trigger( 'reset' ); } } if ( 1 == response.data.settings.hide_complete ) { /** * TODO: This needs to be re-worked for backbone. It's not dynamic enough. */ formModel.trigger( 'hide' ); // jQuery( '.nf-fields' ).hide(); // jQuery( '.nf-form-title' ).hide(); } } }); return controller; } ); define('controllers/fieldProduct',[], function() { var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( nfRadio.channel( 'product' ), 'init:model', this.register ); nfRadio.channel( 'product' ).reply( 'get:calcValue', this.getCalcValue, this ); }, register: function( model ) { model.set( 'renderProductQuantity', this.renderProductQuantity ); model.set( 'renderProduct', this.renderProduct ); model.set( 'renderOptions', this.renderOptions ); }, renderProduct: function(){ switch( this.product_type ) { case 'user': var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-field-textbox' ); return template( this ); break; case 'hidden': var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-field-hidden' ); return template( this ); break; case 'dropdown': var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-product-dropdown' ); return template( this ); break; default: var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-product-single' ); return template( this ); } }, renderProductQuantity: function(){ if ( 1 == this.product_use_quantity ) { var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-product-quantity' ); return template( this ); } }, renderOptions: function() { var that = this; var html = ''; _.each( this.options, function( option ) { if ( 1 == option.selected ) { var selected = true; } else { var selected = false; } option.selected = selected; option.fieldID = that.id; option.classes = that.classes; option.currentValue = that.value; var template = nfRadio.channel( 'app' ).request( 'get:template', '#tmpl-nf-product-' + that.product_type + '-option' ); html += template( option ); } ); return html; }, getCalcValue: function( fieldModel ) { var product_price = fieldModel.get( 'product_price' ); var product_quantity = fieldModel.get( 'value' ); return product_price * product_quantity; } }); return controller; } ); define('controllers/fieldTotal',[], function() { var controller = Marionette.Object.extend( { totalModel: {}, productTotals: {}, initialize: function() { this.listenTo( nfRadio.channel( 'total' ), 'init:model', this.register ); this.listenTo( nfRadio.channel( 'shipping' ), 'init:model', this.registerShipping ); }, register: function( totalModel ){ this.totalModel = totalModel; var formID = totalModel.get( 'formID' ); this.listenTo( nfRadio.channel( 'form-' + formID ), 'loaded', this.onFormLoaded ); this.listenTo( nfRadio.channel( 'product' ), 'change:modelValue', this.onChangeProduct ); this.listenTo( nfRadio.channel( 'quantity' ), 'change:modelValue', this.onChangeQuantity ); }, registerShipping: function( shippingModel ){ this.shippingCost = shippingModel.get( 'shipping_cost' ); }, onFormLoaded: function( formModel ){ var fieldModels = formModel.get( 'fields' ).models; var productFields = {}; var quantityFields = {}; for( var model in fieldModels ){ var field = fieldModels[ model ]; var fieldID = field.get( 'id' ); // TODO: Maybe use switch if( 'product' == field.get( 'type' ) ){ productFields[ fieldID ] = field; } else if( 'quantity' == field.get( 'type' ) ){ var productID = field.get( 'product_assignment' ); quantityFields[ productID ] = field; } } for( var productID in productFields ){ var product = productFields[ productID ]; var productPrice = Number( product.get( 'product_price' ) ); if( quantityFields[ productID ] ){ productPrice *= quantityFields[ productID ].get( 'value' ); } else if( 1 == product.get( 'product_use_quantity' ) ){ productPrice *= product.get( 'value' ); } this.productTotals[ productID ] = productPrice; } this.updateTotal(); }, onChangeProduct: function( model ){ var productID = model.get( 'id' ); var productPrice = Number( model.get( 'product_price' ) ); var productQuantity = Number( model.get( 'value' ) ); var newTotal = productQuantity * productPrice; this.productTotals[ productID ] = newTotal; this.updateTotal(); }, onChangeQuantity: function( model ){ var productID = model.get( 'product_assignment' ); var productField = nfRadio.channel( 'fields' ).request( 'get:field', productID ); var productPrice = Number( productField.get( 'product_price' ) ); var quantity = Number( model.get( 'value' ) ); var newTotal = quantity * productPrice; this.productTotals[ productID ] = newTotal; this.updateTotal(); }, updateTotal: function(){ var newTotal = 0; for( var product in this.productTotals ){ newTotal += Number( this.productTotals[ product ] ); } if( newTotal && this.shippingCost ) { // Only add shipping if there is a cost. newTotal += Number(this.shippingCost); } this.totalModel.set( 'value', newTotal.toFixed( 2 ) ); this.totalModel.trigger( 'reRender' ); } }); return controller; }); define('controllers/fieldQuantity',[], function() { var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( nfRadio.channel( 'quantity' ), 'init:model', this.registerQuantity ); }, registerQuantity: function( model ){ var productID = model.get( 'product_assignment' ); var product = nfRadio.channel( 'fields' ).request( 'get:field', productID ); if( product ) { product.set('product_use_quantity', 0); } }, }); return controller; }); /** * Model that represents a calculation. * * On init, we trigger a radio message so that controllers can do things when a calc model inits. */ define( 'models/calcModel',[], function() { var model = Backbone.Model.extend( { initialize: function() { // Set our form id this.set( 'formID', this.collection.options.formModel.get( 'id' ) ); // Set our initial fields object to empty. This will hold our key/value pairs. this.set( 'fields', {} ); // Trigger a radio message to let controllers know we've inited this model. nfRadio.channel( 'calc' ).trigger( 'init:model', this ); // When we change the value of this calculation, send out a radio message this.on( 'change:value', this.changeValue, this ); }, /** * Trigger a radio message when a field present in our calculation changes * * The listener that triggers/calls this function is in controllers/calculations * * @since 3.0 * @return void */ changeField: function( fieldModel ) { nfRadio.channel( 'calc' ).trigger( 'change:field', this, fieldModel ); }, changeCalc: function( targetCalcModel ) { nfRadio.channel( 'calc' ).trigger( 'change:calc', this, targetCalcModel ); }, changeValue: function() { nfRadio.channel( 'calc' ).trigger( 'change:value', this ); } } ); return model; } ); define( 'models/calcCollection',['models/calcModel'], function( CalcModel ) { var collection = Backbone.Collection.extend( { model: CalcModel, comparator: 'order', initialize: function( models, options ) { this.options = options; _.each( models, function( model ) { if( 'undefined' == typeof model.dec ) return; if ( '' === model.dec.toString().trim() ) model.dec = 2; model.dec = parseInt( model.dec ); } ); /* * Respond to requests for our calc model */ nfRadio.channel( 'form-' + options.formModel.get( 'id' ) ).reply( 'get:calc', this.getCalc, this ); }, getCalc: function( key ) { return this.findWhere( { name: key } ); } } ); return collection; } ); /** * Controller responsible for keeping up with calculations. */ define('controllers/calculations',['models/calcCollection'], function( CalcCollection ) { var controller = Marionette.Object.extend( { initialize: function() { this.calcs = {}; this.displayFields = {}; // When our form initialises, check to see if there are any calculations that need to be tracked. this.listenTo( nfRadio.channel( 'form' ), 'loaded', this.registerCalcs ); // When our collection gets reset, reset calculation tracking as well. this.listenTo( nfRadio.channel( 'fields' ), 'reset:collection', this.resetCalcs ); // When a calc model is initialised, run a setup function. // this.listenTo( nfRadio.channel( 'calc' ), 'init:model', this.setupCalc ); // When a field referenced by a calc model changes, update our calc. this.listenTo( nfRadio.channel( 'calc' ), 'change:field', this.changeField ); // When a calculation referenced by a calc model changes, update our calc. this.listenTo( nfRadio.channel( 'calc' ), 'change:calc', this.changeCalc ); /* * Listen to our field model init for fields that want to display calc values. * If that field has a calc merge tag, replace it with the default calc value. */ var that = this; _.each( nfFrontEnd.use_merge_tags.calculations, function( fieldType ) { that.listenTo( nfRadio.channel( 'fields-' + fieldType ), 'init:model', that.initDisplayField ); } ); // When we change our calc value, update any display fields. this.listenTo( nfRadio.channel( 'calc' ), 'change:value', this.updateDisplayFields ); // Set an init variable so that we only call reRender on the display field on change, not on init. this.init = {}; }, /** * Passthrough function to reset tracking of calculations when the fieldCollection is reset. * * @since 3.2 * @param backbone.collection fieldCollection * @return void */ resetCalcs: function( fieldCollection ) { if( 'undefined' != typeof( fieldCollection.options.formModel ) ) { this.registerCalcs( fieldCollection.options.formModel ); } }, /** * When our form loads, create a collection out of any calculations. * * @since 3.0 * @param backbone.model formModel * @return void */ registerCalcs: function( formModel ) { var calcCollection = new CalcCollection( formModel.get( 'settings' ).calculations, { formModel: formModel } ); this.calcs[ formModel.get( 'id' ) ] = calcCollection; var that = this; _.each( calcCollection.models, function( calcModel ) { /* * We set a property on our init variable for the calc model we're looping over. * This property is set to true so that when we make changes to the calc model on the next line * the field view doesn't try to redraw itself. * If we don't do this, the 'reRender' attribute of the model will be set before the view is initialized, * which means that setting 'reRender' to true will never re-render the view. */ that.init[ calcModel.get( 'name' ) ] = true; // Setup our calculation models with initial values and register listeners for calc-related fields. that.setupCalc( calcModel ); } ); }, /** * When a calculation model is instantiated from the registerCalcs function: * * Use a regex to get an array of the field keys * Setup an initial key/values array * Check for any references to other calculations * Set the initial value of our calculation * * @since 3.0 * @param backbone.model calcModel * @return void */ setupCalc: function( calcModel ) { // Setup our that var so we can access 'this' context in our loop. var that = this; // Get our equation var eq = calcModel.get( 'eq' ); // We want to keep our original eq intact, so we use a different var for string replacment. var eqValues = eq; // Store the name for debugging later. var calcName = calcModel.get( 'name' ); /* TODO: * It might be possible to refactor these two if statements. * The difficulty is that each has a different method of retreiving the specific data model. */ // Check to see if we have any field merge tags in our equation. var fields = eq.match( new RegExp( /{field:(.*?)}/g ) ); if ( fields ) { /* * fields is now an array of field keys that looks like: * ['{field:key'], ['{field:key'], etc. * * We need to run a function with each of our field keys to setup our field key array and hook up our field change listner. */ fields = fields.map( function( field ) { // field will be {field:key} var key = field.replace( ':calc}', '' ).replace( '}', '' ).replace( '{field:', '' ); // Get our field model fieldModel = nfRadio.channel( 'form-' + calcModel.get( 'formID' ) ).request( 'get:fieldByKey', key ); if( 'undefined' == typeof fieldModel ) return; fieldModel.set( 'clean', false ); // Register a listener in our field model for value changes. fieldModel.on( 'change:value', calcModel.changeField, calcModel ); // Get our calc value from our field model. var calcValue = that.getCalcValue( fieldModel ); // Add this field to our internal key/value object. that.updateCalcFields( calcModel, key, calcValue ); // Update the string tracking our merged eq with the calc value. eqValues = that.replaceKey( 'field', key, calcValue, eqValues ); } ); } // Check to see if we have any calc merge tags in our equation. var calcs = eq.match( new RegExp( /{calc:(.*?)}/g ) ); if ( calcs ) { /* * calcs is now an array of calc keys that looks like: * ['{calc:key'], ['{calc:key'], etc. * * We need to run a function with each of our calc keys to setup our calc key array and hook up our calc change listner. */ calcs = calcs.map( function( calc ) { // calc will be {calc:name} var name = calc.replace( '}', '' ).replace( '{calc:', '' ); // Get our calc model var targetCalcModel = calcModel.collection.findWhere( { name: name } ); if( 'undefined' == typeof targetCalcModel ) return; // Listen for changes on our calcluation, since we need to update our calc when it changes. targetCalcModel.on( 'change:value', calcModel.changeCalc, calcModel ); // // Get our calc value from our calc model. var calcValue = targetCalcModel.get( 'value' ); // Update the string tracking our merged eq with the calc value. eqValues = that.replaceKey( 'calc', name, calcValue, eqValues ); } ); } // Scrub unmerged tags (ie deleted/nox-existent fields/calcs, etc). eqValues = eqValues.replace( /{([a-zA-Z0-9]|:|_|-)*}/g, 0 ); // Scrub line breaks. eqValues = eqValues.replace( /\r?\n|\r/g, '' ); // Evaluate the equation and update the value of this model. try { this.debug('Calculation Decoder ' + eqValues + ' -> ' + this.localeDecodeEquation(eqValues) + ' (Setup)'); calcModel.set( 'value', Number( mexp.eval( this.localeDecodeEquation(eqValues) ) ).toFixed( calcModel.get( 'dec' ) ) ); } catch( e ) { //console.log( calcName ); console.log( e ); } // If for whatever reason, we got NaN, reset that to 0. if( calcModel.get( 'value' ) === 'NaN' ) calcModel.set( 'value', '0' ); // Debugging console statement. // console.log( eqValues + ' = ' + calcModel.get( 'value' ) ); }, /** * Update an item in our key/value pair that represents our fields and calc values. * * @since 3.0 * @param backbone.model calcModel * @param string key * @param string calcValue * @return void */ updateCalcFields: function( calcModel, key, calcValue ) { var fields = calcModel.get( 'fields' ); fields[ key ] = calcValue; calcModel.set( 'fields', fields ); }, /** * Get a calc value from a field model. * * Sends a request to see if there's a special calc value * Uses the value of the field if there is not. * * @since 3.0 * @param backbone.model fieldModel * @return value */ getCalcValue: function( fieldModel ) { /* * Send out a request on the field type and parent type channel asking if they need to modify the calc value. * This is helpful for fields like lists that can have a different calc_value than selected value. */ var value = nfRadio.channel( fieldModel.get( 'type' ) ).request( 'get:calcValue', fieldModel ); var localeConverter = new nfLocaleConverter(nfi18n.siteLocale, nfi18n.thousands_sep, nfi18n.decimal_point); var calcValue = value || fieldModel.get( 'value' ); var machineNumber = localeConverter.numberDecoder(calcValue); var formattedNumber = localeConverter.numberEncoder(calcValue); if ( 'undefined' !== typeof machineNumber && jQuery.isNumeric( machineNumber ) ) { value = formattedNumber; } else { value = 0; } // } if ( ! fieldModel.get( 'visible' ) ) { value = 0; } return value; }, /** * Replace instances of key with calcValue. This is used to replace one key at a time. * * If no eq is passed, use calcModel eq. * * Returns a string with instances of key replaced with calcValue. * * @since version * @param string key * @param string calcValue * @param string eq * @return string eq */ replaceKey: function( type, key, calcValue, eq ) { eq = eq || calcModel.get( 'eq' ); tag = '{' + type + ':' + key + '}'; var reTag = new RegExp( tag, 'g' ); calcTag = '{' + type + ':' + key + ':calc}'; var reCalcTag = new RegExp( calcTag, 'g' ); eq = eq.replace( reTag, calcValue ); eq = eq.replace( reCalcTag, calcValue ); return eq; }, /** * Takes a calcModel and returns a string eq with all keys replaced by their appropriate calcValues. * * @since 3.0 * @param backbone.model calcModel * @return string eq */ replaceAllKeys: function( calcModel ) { var eq = calcModel.get( 'eq' ); var that = this; _.each( calcModel.get( 'fields' ), function( value, key ) { eq = that.replaceKey( 'field', key, value, eq ); } ); // If we have any calc merge tags, replace those as well. var calcs = eq.match( new RegExp( /{calc:(.*?)}/g ) ); if ( calcs ) { _.each( calcs, function( calc ) { // calc will be {calc:key} var name = calc.replace( '}', '' ).replace( '{calc:', '' ); var targetCalcModel = calcModel.collection.findWhere( { name: name } ); if( 'undefined' == typeof targetCalcModel ) return; var re = new RegExp( calc, 'g' ); eq = eq.replace( re, targetCalcModel.get( 'value' ) ); } ); } return eq; }, /** * Function that's called when a field within the calculation changes. * * @since 3.0 * @param backbone.model calcModel * @param backbone.model fieldModel * @return void */ changeField: function( calcModel, fieldModel ) { var key = fieldModel.get( 'key' ); var value = this.getCalcValue( fieldModel ); this.updateCalcFields( calcModel, key, value ); var eqValues = this.replaceAllKeys( calcModel ); // Scrub unmerged tags (ie deleted/nox-existent fields/calcs, etc). eqValues = eqValues.replace( /{([a-zA-Z0-9]|:|_|-)*}/g, '0' ); eqValues = eqValues.replace( /\r?\n|\r/g, '' ); try { this.debug('Calculation Decoder ' + eqValues + ' -> ' + this.localeDecodeEquation(eqValues) + ' (Change Field)'); calcModel.set( 'value', Number( mexp.eval( this.localeDecodeEquation(eqValues) ) ).toFixed( calcModel.get( 'dec' ) ) ); } catch( e ) { if(this.debug())console.log( e ); } if( calcModel.get( 'value' ) === 'NaN' ) calcModel.set( 'value', '0' ); // Debugging console statement. // console.log( eqValues + ' = ' + calcModel.get( 'value' ) ); }, initDisplayField: function( fieldModel ) { if( ! fieldModel.get( 'default' ) || 'string' != typeof fieldModel.get( 'default' ) ) return; var calcs = fieldModel.get( 'default' ).match( new RegExp( /{calc:(.*?)}/g ) ); if ( calcs ) { _.each( calcs, function( calcName ) { calcName = calcName.replace( '{calc:', '' ).replace( '}', '' ).replace( ':2', '' ); this.displayFields[ calcName ] = this.displayFields[ calcName ] || []; this.displayFields[ calcName ].push( fieldModel ); }, this ); } }, updateDisplayFields: function( calcModel ) { var that = this; if ( 'undefined' != typeof this.displayFields[ calcModel.get( 'name' ) ] ) { _.each( this.displayFields[ calcModel.get( 'name' ) ], function( fieldModel ) { var value = ''; /** * if we have a html field, we want to use the actual * value and re-evaluate **/ if( "html" === fieldModel.get( 'type' ) ) { value = fieldModel.get( 'value' ); } else { // if not a html field, use default to re-evaluate value = fieldModel.get( 'default' ); } /* This is a fix for the issue of the merge tags being display'd */ // Find spans with calc data-key values var spans = value.match( new RegExp( /(.*?)<\/span>/, "" ) + "}"; value = value.replace( spanVar, tmpCalcTag ); } ); var calcs = value.match( new RegExp( /{calc:(.*?)}/g ) ); _.each( calcs, function( calc ) { // var rounding = false; // calc will be {calc:key} or {calc:key:2} var name = calc.replace( '}', '' ).replace( '{calc:', '' ).replace( ':2', '' ); /* * TODO: Bandaid for rounding calculations to two decimal places when displaying the merge tag. * Checks to see if we have a :2. If we do, remove it and set our rounding variable to true. */ // if ( -1 != name.indexOf( ':2' ) ) { // rounding = true; // name = name.replace( ':2', '' ); // } var calcModel = that.calcs[ fieldModel.get( 'formID' ) ].findWhere( { name: name } ); var re = new RegExp( calc, 'g' ); var calcValue = calcModel.get( 'value' ) ; // if ( rounding ) { // calcValue = calcValue.toFixed( 2 ); // rounding = false; // } if( 'undefined' != typeof( calcValue ) ) { calcValue = that.applyLocaleFormatting( calcValue, calcModel ); } /* * We replace the merge tag with the value * surrounded by a span so that we can still find it * and not affect itself or other field merge tags * * Unless this isn't a html field, then we just set * value to calcValue */ if( "html" === fieldModel.get( 'type' ) ) { value = value.replace(re, "" + calcValue + ""); } else { value = calcValue; } } ); fieldModel.set( 'value', value ); if ( ! that.init[ calcModel.get( 'name' ) ] ) { // fieldModel.set( 'reRender', true ); fieldModel.trigger( 'reRender' ); } that.init[ calcModel.get( 'name' ) ] = false; } ); } }, getCalc: function( name, formID ) { return this.calcs[ formID ].findWhere( { name: name } ); }, changeCalc: function( calcModel, targetCalcModel ) { var eqValues = this.replaceAllKeys( calcModel ); eqValues = eqValues.replace( '[', '' ).replace( ']', '' ); eqValues = eqValues.replace( /\r?\n|\r/g, '' ); try { this.debug('Calculation Decoder ' + eqValues + ' -> ' + this.localeDecodeEquation(eqValues) + ' (Change Calc)'); calcModel.set( 'value', Number( mexp.eval( this.localeDecodeEquation( eqValues ) ) ).toFixed( calcModel.get( 'dec' ) ) ); } catch( e ) { console.log( e ); } if( calcModel.get( 'value' ) === 'NaN' ) calcModel.set( 'value', '0' ); }, /** * Function to apply Locale Formatting to Calculations * @since Version 3.1 * @param Str number * * @return Str */ applyLocaleFormatting: function( number, calcModel ) { var localeConverter = new nfLocaleConverter(nfi18n.siteLocale, nfi18n.thousands_sep, nfi18n.decimal_point); var formattedNumber = localeConverter.numberEncoder(number, calcModel.get('dec')); // // Split our string on the decimal to preserve context. // var splitNumber = number.split('.'); // // If we have more than one element (if we had a decimal point)... // if ( splitNumber.length > 1 ) { // // Update the thousands and remerge the array. // splitNumber[ 0 ] = splitNumber[ 0 ].replace( /\B(?=(\d{3})+(?!\d))/g, nfi18n.thousands_sep ); // var formattedNumber = splitNumber.join( nfi18n.decimal_point ); // } // // Otherwise (we had no decimal point)... // else { // // Update the thousands. // var formattedNumber = number.replace( /\B(?=(\d{3})+(?!\d))/g, nfi18n.thousands_sep ); // } return formattedNumber; }, localeDecodeEquation: function( eq ) { var result = ''; var expression = ''; var pattern = /[0-9.,]/; var localeConverter = new nfLocaleConverter(nfi18n.siteLocale, nfi18n.thousands_sep, nfi18n.decimal_point); // This pattern accounts for all whitespace characters (including thin space). eq = eq.replace( /\s/g, '' ); eq = eq.replace( / /g, '' ); var characters = eq.split(''); // foreach ( characters as character ) { characters.forEach( function( character ) { // If the character is numeric or '.' or ',' if (pattern.test(character)) { expression = expression + character; } else { // If we reach an operator char, append the expression to the result if ( 0 < expression.length ) { result = result + localeConverter.numberDecoder( expression ); expression = ''; } result = result + character; } }); // The following catches the case of the last character being a digit. if ( 0 < expression.length ) { result = result + localeConverter.numberDecoder( expression ); } return result; }, debug: function(message) { if ( window.nfCalculationsDebug || false ) console.log(message); } }); return controller; } ); define('controllers/dateBackwardsCompat',[], function() { var controller = Marionette.Object.extend({ initialize: function () { this.listenTo( Backbone.Radio.channel( 'pikaday-bc' ), 'init', this.dateBackwardsCompat ); }, dateBackwardsCompat: function( dateObject, fieldModel ) { /** * Start backwards compatibility for old pikaday customisation */ // Legacy properties dateObject.pikaday = {}; dateObject.pikaday._o = {}; //Old hook for Pikaday Custom code nfRadio.channel( 'pikaday' ).trigger( 'init', dateObject, fieldModel ); // If we've set a disableDayFn property in custom code, hook it up to Flatpickr if ( typeof dateObject.pikaday._o.disableDayFn !== 'undefined') { dateObject.set( 'disable', [ dateObject.pikaday._o.disableDayFn ] ); } //Compatibility for i18n pikaday function if ( typeof dateObject.pikaday._o.i18n !== 'undefined' || typeof dateObject.pikaday._o.firstDay !== 'undefined') { let locale = dateObject.config.locale; if ( typeof dateObject.pikaday._o.firstDay !== 'undefined') { locale.firstDayOfWeek = dateObject.pikaday._o.firstDay; } if ( typeof dateObject.pikaday._o.i18n !== 'undefined') { if ( typeof dateObject.pikaday._o.i18n.weekdays !== 'undefined') { locale.weekdays.longhand = dateObject.pikaday._o.i18n.weekdays; } if ( typeof dateObject.pikaday._o.i18n.weekdaysShort !== 'undefined') { locale.weekdays.shorthand = dateObject.pikaday._o.i18n.weekdaysShort; } if ( typeof dateObject.pikaday._o.i18n.months !== 'undefined') { jQuery( '.flatpickr-monthDropdown-months > option' ).each( function() { this.text = dateObject.pikaday._o.i18n.months[ this.value ]; } ); } } dateObject.set( 'locale', locale ); } if ( Object.keys(dateObject.pikaday._o).length > 0 ) { console.log("%cDeprecated Ninja Forms Pikaday custom code detected.", "color: Red; font-size: large"); console.log("You are using deprecated Ninja Forms Pikaday custom code. Support for this custom code will be removed in a future version of Ninja Forms. Please contact Ninja Forms support for more details."); } } }); return controller; }); define('controllers/fieldDate',[], function() { var controller = Marionette.Object.extend({ initialize: function () { this.listenTo( nfRadio.channel( 'date' ), 'init:model', this.registerFunctions ); this.listenTo( nfRadio.channel( 'date' ), 'render:view', this.initDatepicker ); }, registerFunctions: function( model ) { model.set( 'renderHourOptions', this.renderHourOptions ); model.set( 'renderMinuteOptions', this.renderMinuteOptions ); model.set( 'maybeRenderAMPM', this.maybeRenderAMPM ); model.set( 'customClasses', this.customClasses ); // Overwrite the default getValue() method. model.getValue = this.getValue; }, renderHourOptions: function() { return this.hours_options; }, renderMinuteOptions: function() { return this.minutes_options; }, maybeRenderAMPM: function() { if ( 'undefined' == typeof this.hours_24 || 1 == this.hours_24 ) { return; } return '
'; }, initDatepicker: function ( view ) { view.model.set( 'el', view.el ); var el = jQuery( view.el ).find( '.nf-element' )[0]; view.listenTo( nfRadio.channel( 'form-' + view.model.get( 'formID' ) ), 'before:submit', this.beforeSubmit, view ); // If we are using a time_only date_mode, then hide the date input. if ( 'undefined' != typeof view.model.get( 'date_mode' ) && 'time_only' == view.model.get( 'date_mode' ) ) { jQuery( el ).hide(); return false; } var dateFormat = view.model.get( 'date_format' ); // For "default" date format, convert PHP format to JS compatible format. if( '' == dateFormat || 'default' == dateFormat ){ dateFormat = this.convertDateFormat( nfi18n.dateFormat ); } var dateSettings = { classes: jQuery( el ).attr( "class" ), placeholder: view.model.get( 'placeholder' ), parseDate: function (datestr, format) { return moment(datestr, format, true).toDate(); }, formatDate: function (date, format, locale) { return moment(date).format(format); }, dateFormat: dateFormat, altFormat: dateFormat, altInput: true, ariaDateFormat: dateFormat, mode: "single", allowInput: true, disableMobile: "true", locale: { months: { shorthand: nfi18n.monthsShort, longhand: nfi18n.months }, weekdays: { shorthand: nfi18n.weekdaysShort, longhand: nfi18n.weekdays }, firstDayOfWeek: nfi18n.startOfWeek, } }; // Filter our datepicker settings object. let filteredDatePickerSettings = nfRadio.channel( 'flatpickr' ).request( 'filter:settings', dateSettings, view ); if ( 'undefined' != typeof filteredDatePickerSettings ) { dateSettings = filteredDatePickerSettings; } var dateObject = flatpickr( el, dateSettings ); if ( 1 == view.model.get( 'date_default' ) ) { dateObject.setDate( moment().format(dateFormat) ); view.model.set( 'value', moment().format(dateFormat) ); } //Trigger Pikaday backwards compatibility nfRadio.channel( 'pikaday-bc' ).trigger( 'init', dateObject, view.model, view ); nfRadio.channel( 'flatpickr' ).trigger( 'init', dateObject, view.model, view ); }, beforeSubmit: function( formModel ) { if ( 'date_only' == this.model.get( 'date_mode' ) ) { return false; } let hour = jQuery( this.el ).find( '.hour' ).val(); let minute = jQuery( this.el ).find( '.minute' ).val(); let ampm = jQuery( this.el ).find( '.ampm' ).val(); let current_value = this.model.get( 'value' ); let date = false; if ( _.isObject( current_value ) ) { date = current_value.date; } else { date = current_value; } let date_value = { date: date, hour: hour, minute: minute, ampm: ampm, }; this.model.set( 'value', date_value ); }, getYearRange: function( fieldModel ) { var yearRange = 10; var yearRangeStart = fieldModel.get( 'year_range_start' ); var yearRangeEnd = fieldModel.get( 'year_range_end' ); if( yearRangeStart && yearRangeEnd ){ return [ yearRangeStart, yearRangeEnd ]; } else if( yearRangeStart ) { yearRangeEnd = yearRangeStart + yearRange; return [ yearRangeStart, yearRangeEnd ]; } else if( yearRangeEnd ) { yearRangeStart = yearRangeEnd - yearRange; return [ yearRangeStart, yearRangeEnd ]; } return yearRange; }, getMinDate: function( fieldModel ) { var minDate = null; var yearRangeStart = fieldModel.get( 'year_range_start' ); if( yearRangeStart ) { return new Date( yearRangeStart, 0, 1 ); } return minDate; }, getMaxDate: function( fieldModel ) { var maxDate = null; var yearRangeEnd = fieldModel.get( 'year_range_end' ); if( yearRangeEnd ) { return new Date( yearRangeEnd, 11, 31 ); } return maxDate; }, convertDateFormat: function( dateFormat ) { // http://php.net/manual/en/function.date.php // https://github.com/dbushell/Pikaday/blob/master/README.md#formatting **** Switched to flatpickr *** // Note: Be careful not to add overriding replacements. Order is important here. /** Day */ dateFormat = dateFormat.replace( 'D', 'ddd' ); // @todo Ordering issue? dateFormat = dateFormat.replace( 'd', 'DD' ); dateFormat = dateFormat.replace( 'l', 'dddd' ); dateFormat = dateFormat.replace( 'j', 'D' ); dateFormat = dateFormat.replace( 'N', '' ); // Not Supported dateFormat = dateFormat.replace( 'S', '' ); // Not Supported dateFormat = dateFormat.replace( 'w', 'd' ); dateFormat = dateFormat.replace( 'z', '' ); // Not Supported /** Week */ dateFormat = dateFormat.replace( 'W', 'W' ); /** Month */ dateFormat = dateFormat.replace( 'M', 'MMM' ); // "M" before "F" or "m" to avoid overriding. dateFormat = dateFormat.replace( 'F', 'MMMM' ); dateFormat = dateFormat.replace( 'm', 'MM' ); dateFormat = dateFormat.replace( 'n', 'M' ); dateFormat = dateFormat.replace( 't', '' ); // Not Supported // Year dateFormat = dateFormat.replace( 'L', '' ); // Not Supported dateFormat = dateFormat.replace( 'o', 'YYYY' ); dateFormat = dateFormat.replace( 'Y', 'YYYY' ); dateFormat = dateFormat.replace( 'y', 'YY' ); // Time - Not supported dateFormat = dateFormat.replace( 'a', '' ); dateFormat = dateFormat.replace( 'A', '' ); dateFormat = dateFormat.replace( 'B', '' ); dateFormat = dateFormat.replace( 'g', '' ); dateFormat = dateFormat.replace( 'G', '' ); dateFormat = dateFormat.replace( 'h', '' ); dateFormat = dateFormat.replace( 'H', '' ); dateFormat = dateFormat.replace( 'i', '' ); dateFormat = dateFormat.replace( 's', '' ); dateFormat = dateFormat.replace( 'u', '' ); dateFormat = dateFormat.replace( 'v', '' ); // Timezone - Not supported dateFormat = dateFormat.replace( 'e', '' ); dateFormat = dateFormat.replace( 'I', '' ); dateFormat = dateFormat.replace( 'O', '' ); dateFormat = dateFormat.replace( 'P', '' ); dateFormat = dateFormat.replace( 'T', '' ); dateFormat = dateFormat.replace( 'Z', '' ); // Full Date/Time - Not Supported dateFormat = dateFormat.replace( 'c', '' ); dateFormat = dateFormat.replace( 'r', '' ); dateFormat = dateFormat.replace( 'u', '' ); return dateFormat; }, customClasses: function( classes ) { if ( 'date_and_time' == this.date_mode ) { classes += ' date-and-time'; } return classes; }, // This function is called whenever we want to know the value of the date field. // Since it could be a date/time field, we can't return just the value. getValue: function() { if ( 'date_only' == this.get( 'date_mode' ) ) { return this.get( 'value' ); } let el = this.get( 'el' ); let hour = jQuery( el ).find( '.hour' ).val(); let minute = jQuery( el ).find( '.minute' ).val(); let ampm = jQuery( el ).find( '.ampm' ).val(); let current_value = this.get( 'value' ); let date = false; if ( _.isObject( current_value ) ) { date = current_value.date; } else { date = current_value; } let value = ''; if ( 'undefined' != typeof date ) { value += date; } if ( 'undefined' != typeof hour && 'undefined' != typeof minute ) { value += ' ' + hour + ':' + minute; } if ( 'undefined' != typeof ampm ) { value += ' ' + ampm; } return value; // let date_value = { // date: date, // hour: hour, // minute: minute, // ampm: ampm, // }; // this.model.set( 'value', date_value ); } }); return controller; }); define('controllers/fieldRecaptcha',[], function() { var controller = Marionette.Object.extend({ initialize: function () { this.listenTo( nfRadio.channel( 'recaptcha' ), 'init:model', this.initRecaptcha ); this.listenTo( nfRadio.channel( 'forms' ), 'submit:response', this.resetRecaptcha ); }, initRecaptcha: function ( model ) { nfRadio.channel( 'recaptcha' ).reply( 'update:response', this.updateResponse, this, model.id ); }, updateResponse: function( response, fieldID ) { var model = nfRadio.channel( 'fields' ).request( 'get:field', fieldID ); model.set( 'value', response ); nfRadio.channel( 'fields' ).request( 'remove:error', model.get( 'id' ), 'required-error' ); }, resetRecaptcha: function() { var recaptchaID = 0; jQuery( '.g-recaptcha' ).each( function() { try { grecaptcha.reset( recaptchaID ); } catch( e ){ console.log( 'Notice: Error trying to reset grecaptcha.' ); } recaptchaID++; } ); } }); return controller; } ); define('controllers/fieldRecaptchaV3',[], function() { var controller = Marionette.Object.extend({ initialize: function () { this.listenTo( nfRadio.channel( 'recaptcha_v3' ), 'init:model', this.initRecaptcha ); }, initRecaptcha: function ( model ) { let formID = model.get( 'formID' ); nfRadio.channel( 'form-' + formID ).trigger( 'disable:submit', model ); grecaptcha.ready( function() { grecaptcha.execute( model.get( 'site_key' ), { action: 'register' } ).then( function( token ) { model.set( 'value', token ); nfRadio.channel( 'form-' + formID ).trigger( 'enable:submit', model ); } ); } ); }, }); return controller; } ); define('controllers/fieldHTML',[], function() { var controller = Marionette.Object.extend({ htmlFields: [], trackedMergeTags: [], initialize: function () { this.listenTo( Backbone.Radio.channel( 'fields-html' ), 'init:model', this.setupFieldMergeTagTracking ); }, setupFieldMergeTagTracking: function( fieldModel ) { this.htmlFields.push( fieldModel ); var formID = fieldModel.get( 'formID' ); this.listenTo( nfRadio.channel( 'form-' + formID ), 'init:model', function( formModel ){ var mergeTags = fieldModel.get( 'default' ).match( new RegExp( /{field:(.*?)}/g ) ); if ( ! mergeTags ) return; _.each( mergeTags, function( mergeTag ) { var fieldKey = mergeTag.replace( '{field:', '' ).replace( '}', '' ); var fieldModel = formModel.get( 'fields' ).findWhere({ key: fieldKey }); if( 'undefined' == typeof fieldModel ) return; this.trackedMergeTags.push( fieldModel ); this.listenTo( nfRadio.channel( 'field-' + fieldModel.get( 'id' ) ), 'change:modelValue', this.updateFieldMergeTags ); }, this ); // Let's get this party started! this.updateFieldMergeTags(); }, this ); }, updateFieldMergeTags: function( fieldModel ) { _.each( this.htmlFields, function( htmlFieldModel ){ var value = htmlFieldModel.get( 'value' ); _.each( this.trackedMergeTags, function( fieldModel ){ /* Search the value for any spans with mergetag data-key * values */ var spans = value.match( new RegExp( /" + fieldModel.getValue() + "" ); }, this ) ; htmlFieldModel.set( 'value', value ); htmlFieldModel.trigger( 'reRender' ); }, this ); } }); return controller; }); /** * When a form is loaded, enable any help text that appears on the page. */ define('controllers/helpText',[], function() { var controller = Marionette.Object.extend( { initialize: function() { this.listenTo( nfRadio.channel( 'form' ), 'render:view', this.initHelpText ); nfRadio.channel( 'form' ).reply( 'init:help', this.initHelpText ); }, initHelpText: function( view ) { jQuery( view.el ).find( '.nf-help' ).each( function() { var jBox = jQuery( this ).jBox( 'Tooltip', { theme: 'TooltipBorder', content: jQuery( this ).data( 'text' ) }); } ); } }); return controller; } ); define('controllers/fieldTextbox',[], function() { var controller = Marionette.Object.extend( { initialize: function() { nfRadio.channel( 'textbox' ).reply( 'get:calcValue', this.getCalcValue, this ); }, getCalcValue: function( fieldModel ) { if('currency' == fieldModel.get('mask')){ var form = nfRadio.channel( 'app' ).request( 'get:form', fieldModel.get( 'formID' ) ); var currencySymbol = ('undefined' !== typeof form) ? form.get( 'currencySymbol' ) : ''; var currencySymbolDecoded = jQuery('